diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index 4650c1e64..7ad25dff0 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -27,8 +27,9 @@ "AUTOMATIC_POOLS_UPDATE": false, "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json", "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", + "POOLS_UPDATE_DELAY": 604800, "AUDIT": false, - "RUST_GBT": false, + "RUST_GBT": true, "LIMIT_GBT": false, "CPFP_INDEXING": false, "DISK_CACHE_BLOCK_INTERVAL": 6, @@ -45,7 +46,8 @@ "PASSWORD": "mempool", "TIMEOUT": 60000, "COOKIE": false, - "COOKIE_PATH": "/path/to/bitcoin/.cookie" + "COOKIE_PATH": "/path/to/bitcoin/.cookie", + "DEBUG_LOG_PATH": "/path/to/bitcoin/debug.log" }, "ELECTRUM": { "HOST": "127.0.0.1", diff --git a/backend/package-lock.json b/backend/package-lock.json index 126660166..e0d28bfc9 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,10 +13,10 @@ "@babel/core": "^7.25.2", "@mempool/electrum-client": "1.1.9", "@types/node": "^18.15.3", - "axios": "~1.7.4", + "axios": "1.7.2", "bitcoinjs-lib": "~6.1.3", "crypto-js": "~4.2.0", - "express": "~4.19.2", + "express": "~4.21.1", "maxmind": "~4.3.11", "mysql2": "~3.11.0", "redis": "^4.7.0", @@ -2278,9 +2278,10 @@ } }, "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -2489,9 +2490,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -2501,7 +2502,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -2826,9 +2827,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "engines": { "node": ">= 0.6" } @@ -3030,9 +3031,9 @@ "dev": true }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "engines": { "node": ">= 0.8" } @@ -3460,36 +3461,36 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -3602,12 +3603,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -6051,9 +6052,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -6267,9 +6271,12 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6437,9 +6444,9 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/path-type": { "version": "4.0.0", @@ -6647,11 +6654,11 @@ ] }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -6872,9 +6879,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -6907,6 +6914,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6918,14 +6933,14 @@ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -9439,9 +9454,9 @@ "integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ==" }, "axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "requires": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -9604,9 +9619,9 @@ } }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "requires": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -9616,7 +9631,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -9850,9 +9865,9 @@ "dev": true }, "cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" }, "cookie-signature": { "version": "1.0.6", @@ -9997,9 +10012,9 @@ "dev": true }, "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" }, "error-ex": { "version": "1.3.2", @@ -10304,36 +10319,36 @@ } }, "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -10435,12 +10450,12 @@ } }, "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "requires": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -12237,9 +12252,9 @@ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" }, "merge-stream": { "version": "2.0.0", @@ -12402,9 +12417,9 @@ } }, "object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" }, "on-finished": { "version": "2.4.1", @@ -12521,9 +12536,9 @@ "dev": true }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "path-type": { "version": "4.0.0", @@ -12665,11 +12680,11 @@ "dev": true }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "queue-microtask": { @@ -12803,9 +12818,9 @@ "dev": true }, "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "requires": { "debug": "2.6.9", "depd": "2.0.0", @@ -12837,6 +12852,11 @@ } } }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -12850,14 +12870,14 @@ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "requires": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" } }, "set-function-length": { diff --git a/backend/package.json b/backend/package.json index 51abf2f7b..9ac3f9199 100644 --- a/backend/package.json +++ b/backend/package.json @@ -42,10 +42,10 @@ "@babel/core": "^7.25.2", "@mempool/electrum-client": "1.1.9", "@types/node": "^18.15.3", - "axios": "~1.7.4", + "axios": "1.7.2", "bitcoinjs-lib": "~6.1.3", "crypto-js": "~4.2.0", - "express": "~4.19.2", + "express": "~4.21.1", "maxmind": "~4.3.11", "mysql2": "~3.11.0", "rust-gbt": "file:./rust-gbt", diff --git a/backend/src/__fixtures__/mempool-config.template.json b/backend/src/__fixtures__/mempool-config.template.json index 3796b7f22..a9f246767 100644 --- a/backend/src/__fixtures__/mempool-config.template.json +++ b/backend/src/__fixtures__/mempool-config.template.json @@ -28,6 +28,7 @@ "INDEXING_BLOCKS_AMOUNT": 14, "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__", "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__", + "POOLS_UPDATE_DELAY": 604800, "AUDIT": true, "RUST_GBT": false, "LIMIT_GBT": false, @@ -46,7 +47,8 @@ "PASSWORD": "__CORE_RPC_PASSWORD__", "TIMEOUT": 1000, "COOKIE": false, - "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__" + "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__", + "DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__" }, "ELECTRUM": { "HOST": "__ELECTRUM_HOST__", diff --git a/backend/src/__tests__/api/common.ts b/backend/src/__tests__/api/common.ts index 74a7db88f..14ae3c78b 100644 --- a/backend/src/__tests__/api/common.ts +++ b/backend/src/__tests__/api/common.ts @@ -1,5 +1,5 @@ import { Common } from '../../api/common'; -import { MempoolTransactionExtended } from '../../mempool.interfaces'; +import { MempoolTransactionExtended, TransactionExtended } from '../../mempool.interfaces'; const randomTransactions = require('./test-data/transactions-random.json'); const replacedTransactions = require('./test-data/transactions-replaced.json'); @@ -10,14 +10,14 @@ describe('Common', () => { describe('RBF', () => { const newTransactions = rbfTransactions.concat(randomTransactions); test('should detect RBF transactions with fast method', () => { - const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions); + const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions); expect(Object.values(result).length).toEqual(2); expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); }); test('should detect RBF transactions with scalable method', () => { - const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true); + const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions, true); expect(Object.values(result).length).toEqual(2); expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); diff --git a/backend/src/__tests__/config.test.ts b/backend/src/__tests__/config.test.ts index 050213143..b3cf7e2a7 100644 --- a/backend/src/__tests__/config.test.ts +++ b/backend/src/__tests__/config.test.ts @@ -41,8 +41,9 @@ describe('Mempool Backend Config', () => { STDOUT_LOG_MIN_PRIORITY: 'debug', POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json', + POOLS_UPDATE_DELAY: 604800, AUDIT: false, - RUST_GBT: false, + RUST_GBT: true, LIMIT_GBT: false, CPFP_INDEXING: false, MAX_BLOCKS_BULK_QUERY: 0, @@ -73,7 +74,8 @@ describe('Mempool Backend Config', () => { PASSWORD: 'mempool', TIMEOUT: 60000, COOKIE: false, - COOKIE_PATH: '/bitcoin/.cookie' + COOKIE_PATH: '/bitcoin/.cookie', + DEBUG_LOG_PATH: '', }); expect(config.SECOND_CORE_RPC).toStrictEqual({ diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index a08f43238..e246f249d 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -1,4 +1,4 @@ -import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface'; +import { IBitcoinApi, SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface'; import { IEsploraApi } from './esplora-api.interface'; export interface AbstractBitcoinApi { @@ -23,12 +23,14 @@ export interface AbstractBitcoinApi { $getScriptHashTransactions(address: string, lastSeenTxId: string): Promise; $sendRawTransaction(rawTransaction: string): Promise; $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise; + $submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise; $getOutspend(txId: string, vout: number): Promise; $getOutspends(txId: string): Promise; $getBatchedOutspends(txId: string[]): Promise; $getBatchedOutspendsInternal(txId: string[]): Promise; $getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise; $getCoinbaseTx(blockhash: string): Promise; + $getAddressTransactionSummary(address: string): Promise; startHealthChecks(): void; getHealthStatus(): HealthCheckHost[]; diff --git a/backend/src/api/bitcoin/bitcoin-api.interface.ts b/backend/src/api/bitcoin/bitcoin-api.interface.ts index 6e8583f6f..5d8371d27 100644 --- a/backend/src/api/bitcoin/bitcoin-api.interface.ts +++ b/backend/src/api/bitcoin/bitcoin-api.interface.ts @@ -218,3 +218,21 @@ export interface TestMempoolAcceptResult { }, ['reject-reason']?: string, } + +export interface SubmitPackageResult { + package_msg: string; + "tx-results": { [wtxid: string]: TxResult }; + "replaced-transactions"?: string[]; +} + +export interface TxResult { + txid: string; + "other-wtxid"?: string; + vsize?: number; + fees?: { + base: number; + "effective-feerate"?: number; + "effective-includes"?: string[]; + }; + error?: string; +} diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index 7fa431db6..b78c15bf2 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -1,6 +1,6 @@ import * as bitcoinjs from 'bitcoinjs-lib'; import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory'; -import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface'; +import { IBitcoinApi, SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface'; import { IEsploraApi } from './esplora-api.interface'; import blocks from '../blocks'; import mempool from '../mempool'; @@ -196,6 +196,10 @@ class BitcoinApi implements AbstractBitcoinApi { } } + $submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise { + return this.bitcoindClient.submitPackage(rawTransactions, maxfeerate ?? undefined, maxburnamount ?? undefined); + } + async $getOutspend(txId: string, vout: number): Promise { const txOut = await this.bitcoindClient.getTxOut(txId, vout, false); return { @@ -251,6 +255,10 @@ class BitcoinApi implements AbstractBitcoinApi { return this.$getRawTransaction(txids[0]); } + async $getAddressTransactionSummary(address: string): Promise { + throw new Error('Method getAddressTransactionSummary not supported by the Bitcoin RPC API.'); + } + $getEstimatedHashrate(blockHeight: number): Promise { // 120 is the default block span in Core return this.bitcoindClient.getNetworkHashPs(120, blockHeight); diff --git a/backend/src/api/bitcoin/bitcoin-core.routes.ts b/backend/src/api/bitcoin/bitcoin-core.routes.ts index 7933dc17b..2c3dd08f6 100644 --- a/backend/src/api/bitcoin/bitcoin-core.routes.ts +++ b/backend/src/api/bitcoin/bitcoin-core.routes.ts @@ -1,6 +1,7 @@ import { Application, NextFunction, Request, Response } from 'express'; import logger from '../../logger'; import bitcoinClient from './bitcoin-client'; +import config from '../../config'; /** * Define a set of routes used by the accelerator server @@ -11,15 +12,15 @@ class BitcoinBackendRoutes { public initRoutes(app: Application) { app - .get('/api/internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry) - .post('/api/internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction) - .get('/api/internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction) - .post('/api/internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction) - .post('/api/internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept) - .get('/api/internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors) - .get('/api/internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock) - .get('/api/internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash) - .get('/api/internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount) + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry) + .post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction) + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction) + .post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction) + .post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept) + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors) + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock) + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash) + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount) ; } diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index 6225a9c1d..3b33c1ead 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -20,6 +20,7 @@ import difficultyAdjustment from '../difficulty-adjustment'; import transactionRepository from '../../repositories/TransactionRepository'; import rbfCache from '../rbf-cache'; import { calculateMempoolTxCpfp } from '../cpfp'; +import { handleError } from '../../utils/api'; class BitcoinRoutes { public initRoutes(app: Application) { @@ -47,6 +48,8 @@ class BitcoinRoutes { .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this)) + // Temporarily add txs/package endpoint for all backends until esplora supports it + .post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage) ; if (config.MEMPOOL.BACKEND !== 'esplora') { @@ -86,7 +89,7 @@ class BitcoinRoutes { res.set('Content-Type', 'application/json'); res.send(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -105,13 +108,13 @@ class BitcoinRoutes { const result = mempoolBlocks.getMempoolBlocks(); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private getTransactionTimes(req: Request, res: Response) { if (!Array.isArray(req.query.txId)) { - res.status(500).send('Not an array'); + handleError(req, res, 500, 'Not an array'); return; } const txIds: string[] = []; @@ -128,12 +131,12 @@ class BitcoinRoutes { private async $getBatchedOutspends(req: Request, res: Response): Promise { const txids_csv = req.query.txids; if (!txids_csv || typeof txids_csv !== 'string') { - res.status(500).send('Invalid txids format'); + handleError(req, res, 500, 'Invalid txids format'); return; } const txids = txids_csv.split(','); if (txids.length > 50) { - res.status(400).send('Too many txids requested'); + handleError(req, res, 400, 'Too many txids requested'); return; } @@ -141,13 +144,13 @@ class BitcoinRoutes { const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids); res.json(batchedOutspends); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async $getCpfpInfo(req: Request, res: Response) { if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) { - res.status(501).send(`Invalid transaction ID.`); + handleError(req, res, 501, `Invalid transaction ID.`); return; } @@ -180,7 +183,7 @@ class BitcoinRoutes { try { cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId); } catch (e) { - res.status(500).send('failed to get CPFP info'); + handleError(req, res, 500, 'failed to get CPFP info'); return; } } @@ -209,7 +212,7 @@ class BitcoinRoutes { if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { statusCode = 404; } - res.status(statusCode).send(e instanceof Error ? e.message : e); + handleError(req, res, statusCode, e instanceof Error ? e.message : e); } } @@ -223,7 +226,7 @@ class BitcoinRoutes { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { statusCode = 404; } - res.status(statusCode).send(e instanceof Error ? e.message : e); + handleError(req, res, statusCode, e instanceof Error ? e.message : e); } } @@ -284,13 +287,13 @@ class BitcoinRoutes { // Not modified // 422 Unprocessable Entity // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422 - res.status(422).send(`Psbt had no missing nonWitnessUtxos.`); + handleError(req, res, 422, `Psbt had no missing nonWitnessUtxos.`); } } catch (e: any) { if (e instanceof Error && new RegExp(notFoundError).test(e.message)) { - res.status(404).send(e.message); + handleError(req, res, 404, e.message); } else { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } @@ -304,7 +307,7 @@ class BitcoinRoutes { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { statusCode = 404; } - res.status(statusCode).send(e instanceof Error ? e.message : e); + handleError(req, res, statusCode, e instanceof Error ? e.message : e); } } @@ -314,7 +317,7 @@ class BitcoinRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.json(transactions); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -336,7 +339,7 @@ class BitcoinRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString()); res.json(block); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -346,7 +349,7 @@ class BitcoinRoutes { res.setHeader('content-type', 'text/plain'); res.send(blockHeader); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -357,10 +360,11 @@ class BitcoinRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.json(auditSummary); } else { - return res.status(404).send(`audit not available`); + handleError(req, res, 404, `audit not available`); + return; } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -371,7 +375,8 @@ class BitcoinRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.json(auditSummary); } else { - return res.status(404).send(`transaction audit not available`); + handleError(req, res, 404, `transaction audit not available`); + return; } } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); @@ -388,42 +393,49 @@ class BitcoinRoutes { return await this.getLegacyBlocks(req, res); } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async getBlocksByBulk(req: Request, res: Response) { try { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented - return res.status(404).send(`This API is only available for Bitcoin networks`); + handleError(req, res, 404, `This API is only available for Bitcoin networks`); + return; } if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) { - return res.status(404).send(`This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`); + handleError(req, res, 404, `This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`); + return; } if (!Common.indexingEnabled()) { - return res.status(404).send(`Indexing is required for this API`); + handleError(req, res, 404, `Indexing is required for this API`); + return; } const from = parseInt(req.params.from, 10); if (!req.params.from || from < 0) { - return res.status(400).send(`Parameter 'from' must be a block height (integer)`); + handleError(req, res, 400, `Parameter 'from' must be a block height (integer)`); + return; } const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10); if (to < 0) { - return res.status(400).send(`Parameter 'to' must be a block height (integer)`); + handleError(req, res, 400, `Parameter 'to' must be a block height (integer)`); + return; } if (from > to) { - return res.status(400).send(`Parameter 'to' must be a higher block height than 'from'`); + handleError(req, res, 400, `Parameter 'to' must be a higher block height than 'from'`); + return; } if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) { - return res.status(400).send(`You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`); + handleError(req, res, 400, `You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`); + return; } res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(await blocks.$getBlocksBetweenHeight(from, to)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -458,10 +470,10 @@ class BitcoinRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(returnBlocks); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } - + private async getBlockTransactions(req: Request, res: Response) { try { loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0); @@ -483,7 +495,7 @@ class BitcoinRoutes { res.json(transactions); } catch (e) { loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100); - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -492,13 +504,13 @@ class BitcoinRoutes { const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10)); res.send(blockHash); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async getAddress(req: Request, res: Response) { if (config.MEMPOOL.BACKEND === 'none') { - res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); return; } @@ -507,15 +519,16 @@ class BitcoinRoutes { res.json(addressData); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - return res.status(413).send(e instanceof Error ? e.message : e); + handleError(req, res, 413, e instanceof Error ? e.message : e); + return; } - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async getAddressTransactions(req: Request, res: Response): Promise { if (config.MEMPOOL.BACKEND === 'none') { - res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); return; } @@ -528,23 +541,23 @@ class BitcoinRoutes { res.json(transactions); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - res.status(413).send(e instanceof Error ? e.message : e); + handleError(req, res, 413, e instanceof Error ? e.message : e); return; } - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async getAddressTransactionSummary(req: Request, res: Response): Promise { if (config.MEMPOOL.BACKEND !== 'esplora') { - res.status(405).send('Address summary lookups require mempool/electrs backend.'); + handleError(req, res, 405, 'Address summary lookups require mempool/electrs backend.'); return; } } private async getScriptHash(req: Request, res: Response) { if (config.MEMPOOL.BACKEND === 'none') { - res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); return; } @@ -555,15 +568,16 @@ class BitcoinRoutes { res.json(addressData); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - return res.status(413).send(e instanceof Error ? e.message : e); + handleError(req, res, 413, e instanceof Error ? e.message : e); + return; } - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async getScriptHashTransactions(req: Request, res: Response): Promise { if (config.MEMPOOL.BACKEND === 'none') { - res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); return; } @@ -578,16 +592,16 @@ class BitcoinRoutes { res.json(transactions); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - res.status(413).send(e instanceof Error ? e.message : e); + handleError(req, res, 413, e instanceof Error ? e.message : e); return; } - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async getScriptHashTransactionSummary(req: Request, res: Response): Promise { if (config.MEMPOOL.BACKEND !== 'esplora') { - res.status(405).send('Scripthash summary lookups require mempool/electrs backend.'); + handleError(req, res, 405, 'Scripthash summary lookups require mempool/electrs backend.'); return; } } @@ -597,7 +611,7 @@ class BitcoinRoutes { const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix); res.send(blockHash); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -624,7 +638,7 @@ class BitcoinRoutes { const rawMempool = await bitcoinApi.$getRawMempool(); res.send(rawMempool); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -632,12 +646,13 @@ class BitcoinRoutes { try { const result = blocks.getCurrentBlockHeight(); if (!result) { - return res.status(503).send(`Service Temporarily Unavailable`); + handleError(req, res, 503, `Service Temporarily Unavailable`); + return; } res.setHeader('content-type', 'text/plain'); res.send(result.toString()); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -647,7 +662,7 @@ class BitcoinRoutes { res.setHeader('content-type', 'text/plain'); res.send(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -657,7 +672,7 @@ class BitcoinRoutes { res.setHeader('content-type', 'application/octet-stream'); res.send(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -666,7 +681,7 @@ class BitcoinRoutes { const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -675,7 +690,7 @@ class BitcoinRoutes { const result = await bitcoinClient.validateAddress(req.params.address); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -688,7 +703,7 @@ class BitcoinRoutes { replaces }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -697,7 +712,7 @@ class BitcoinRoutes { const result = rbfCache.getRbfTrees(false); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -706,7 +721,7 @@ class BitcoinRoutes { const result = rbfCache.getRbfTrees(true); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -719,7 +734,7 @@ class BitcoinRoutes { res.status(204).send(); } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -728,7 +743,7 @@ class BitcoinRoutes { const result = await bitcoinApi.$getOutspends(req.params.txId); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -738,10 +753,10 @@ class BitcoinRoutes { if (da) { res.json(da); } else { - res.status(503).send(`Service Temporarily Unavailable`); + handleError(req, res, 503, `Service Temporarily Unavailable`); } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -752,7 +767,7 @@ class BitcoinRoutes { const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx); res.send(txIdResult); } catch (e: any) { - res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) + handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) : (e.message || 'Error')); } } @@ -764,7 +779,7 @@ class BitcoinRoutes { const txIdResult = await bitcoinClient.sendRawTransaction(txHex); res.send(txIdResult); } catch (e: any) { - res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) + handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) : (e.message || 'Error')); } } @@ -776,8 +791,20 @@ class BitcoinRoutes { const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate); res.send(result); } catch (e: any) { - res.setHeader('content-type', 'text/plain'); - res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) + handleError(req, res, 400, e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) + : (e.message || 'Error')); + } + } + + private async $submitPackage(req: Request, res: Response) { + try { + const rawTxs = Common.getTransactionsFromRequest(req); + const maxfeerate = parseFloat(req.query.maxfeerate as string); + const maxburnamount = parseFloat(req.query.maxburnamount as string); + const result = await bitcoinClient.submitPackage(rawTxs, maxfeerate ?? undefined, maxburnamount ?? undefined); + res.send(result); + } catch (e: any) { + handleError(req, res, 400, e.message && e.code ? 'submitpackage RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) : (e.message || 'Error')); } } diff --git a/backend/src/api/bitcoin/esplora-api.interface.ts b/backend/src/api/bitcoin/esplora-api.interface.ts index 6e6860a41..13fb3526d 100644 --- a/backend/src/api/bitcoin/esplora-api.interface.ts +++ b/backend/src/api/bitcoin/esplora-api.interface.ts @@ -179,4 +179,11 @@ export namespace IEsploraApi { burn_count: number; } + export interface AddressTxSummary { + txid: string; + value: number; + height: number; + time: number; + tx_position?: number; + } } diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index b4ae35da9..9a4b7706a 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -5,7 +5,7 @@ import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-fact import { IEsploraApi } from './esplora-api.interface'; import logger from '../../logger'; import { Common } from '../common'; -import { TestMempoolAcceptResult } from './bitcoin-api.interface'; +import { SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface'; interface FailoverHost { host: string, @@ -305,7 +305,7 @@ class ElectrsApi implements AbstractBitcoinApi { } $getAddress(address: string): Promise { - throw new Error('Method getAddress not implemented.'); + return this.failoverRouter.$get('/address/' + address); } $getAddressTransactions(address: string, txId?: string): Promise { @@ -332,6 +332,10 @@ class ElectrsApi implements AbstractBitcoinApi { throw new Error('Method not implemented.'); } + $submitPackage(rawTransactions: string[]): Promise { + throw new Error('Method not implemented.'); + } + $getOutspend(txId: string, vout: number): Promise { return this.failoverRouter.$get('/tx/' + txId + '/outspend/' + vout); } @@ -357,6 +361,10 @@ class ElectrsApi implements AbstractBitcoinApi { return this.failoverRouter.$get('/tx/' + txid); } + async $getAddressTransactionSummary(address: string): Promise { + return this.failoverRouter.$get('/address/' + address + '/txs/summary'); + } + public startHealthChecks(): void { this.failoverRouter.startHealthChecks(); } diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 306179ca5..9a7d8b11a 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -34,6 +34,7 @@ import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp'; import mempool from './mempool'; import CpfpRepository from '../repositories/CpfpRepository'; import accelerationApi from './services/acceleration'; +import { parseDATUMTemplateCreator } from '../utils/bitcoin-script'; class Blocks { private blocks: BlockExtended[] = []; @@ -342,7 +343,12 @@ class Blocks { id: pool.uniqueId, name: pool.name, slug: pool.slug, + minerNames: null, }; + + if (extras.pool.name === 'OCEAN') { + extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw); + } } extras.matchRate = null; diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index d17068a09..50de63afc 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -79,8 +79,8 @@ export class Common { return arr; } - static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } { - const matches: { [txid: string]: MempoolTransactionExtended[] } = {}; + static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} { + const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {}; // For small N, a naive nested loop is extremely fast, but it doesn't scale if (added.length < 1000 && deleted.length < 50 && !forceScalable) { @@ -95,7 +95,7 @@ export class Common { addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout)); }); if (foundMatches?.length) { - matches[addedTx.txid] = [...new Set(foundMatches)]; + matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx }; } }); } else { @@ -123,7 +123,7 @@ export class Common { foundMatches.add(deletedTx); } if (foundMatches.size) { - matches[addedTx.txid] = [...foundMatches]; + matches[addedTx.txid] = { replaced: [...foundMatches], replacedBy: addedTx }; } } } @@ -138,17 +138,17 @@ export class Common { const replaced: Set = new Set(); for (let i = 0; i < tx.vin.length; i++) { const vin = tx.vin[i]; - const match = spendMap.get(`${vin.txid}:${vin.vout}`); + const key = `${vin.txid}:${vin.vout}`; + const match = spendMap.get(key); if (match && match.txid !== tx.txid) { replaced.add(match); // remove this tx from the spendMap // prevents the same tx being replaced more than once for (const replacedVin of match.vin) { - const key = `${replacedVin.txid}:${replacedVin.vout}`; - spendMap.delete(key); + const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`; + spendMap.delete(replacedKey); } } - const key = `${vin.txid}:${vin.vout}`; spendMap.delete(key); } if (replaced.size) { diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 95f8c8707..ee9df9151 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository'; import { RowDataPacket } from 'mysql2'; class DatabaseMigration { - private static currentVersion = 82; + private static currentVersion = 83; private queryTimeout = 3600_000; private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; @@ -705,6 +705,11 @@ class DatabaseMigration { await this.$fixBadV1AuditBlocks(); await this.updateToSchemaVersion(82); } + + if (databaseSchemaVersion < 83 && isBitcoin === true) { + await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL'); + await this.updateToSchemaVersion(83); + } } /** diff --git a/backend/src/api/disk-cache.ts b/backend/src/api/disk-cache.ts index 202f8f4cb..f2a1f2390 100644 --- a/backend/src/api/disk-cache.ts +++ b/backend/src/api/disk-cache.ts @@ -257,6 +257,7 @@ class DiskCache { trees: rbfData.rbf.trees, expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })), mempool: memPool.getMempool(), + spendMap: memPool.getSpendMap(), }); } } catch (e) { diff --git a/backend/src/api/explorer/channels.routes.ts b/backend/src/api/explorer/channels.routes.ts index 391bf628e..8b4c3e8c8 100644 --- a/backend/src/api/explorer/channels.routes.ts +++ b/backend/src/api/explorer/channels.routes.ts @@ -1,6 +1,7 @@ import config from '../../config'; import { Application, Request, Response } from 'express'; import channelsApi from './channels.api'; +import { handleError } from '../../utils/api'; class ChannelsRoutes { constructor() { } @@ -22,7 +23,7 @@ class ChannelsRoutes { const channels = await channelsApi.$searchChannelsById(req.params.search); res.json(channels); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -38,7 +39,7 @@ class ChannelsRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(channel); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -53,11 +54,11 @@ class ChannelsRoutes { const status: string = typeof req.query.status === 'string' ? req.query.status : ''; if (index < -1) { - res.status(400).send('Invalid index'); + handleError(req, res, 400, 'Invalid index'); return; } if (['open', 'active', 'closed'].includes(status) === false) { - res.status(400).send('Invalid status'); + handleError(req, res, 400, 'Invalid status'); return; } @@ -69,14 +70,14 @@ class ChannelsRoutes { res.header('X-Total-Count', channelsCount.toString()); res.json(channels); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async $getChannelsByTransactionIds(req: Request, res: Response): Promise { try { if (!Array.isArray(req.query.txId)) { - res.status(400).send('Not an array'); + handleError(req, res, 400, 'Not an array'); return; } const txIds: string[] = []; @@ -107,7 +108,7 @@ class ChannelsRoutes { res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -119,7 +120,7 @@ class ChannelsRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(channels); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -132,7 +133,7 @@ class ChannelsRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(channels); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } diff --git a/backend/src/api/explorer/general.routes.ts b/backend/src/api/explorer/general.routes.ts index 07620e84a..b4d0c635d 100644 --- a/backend/src/api/explorer/general.routes.ts +++ b/backend/src/api/explorer/general.routes.ts @@ -3,6 +3,8 @@ import { Application, Request, Response } from 'express'; import nodesApi from './nodes.api'; import channelsApi from './channels.api'; import statisticsApi from './statistics.api'; +import { handleError } from '../../utils/api'; + class GeneralLightningRoutes { constructor() { } @@ -27,7 +29,7 @@ class GeneralLightningRoutes { channels: channels, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -41,7 +43,7 @@ class GeneralLightningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(statistics); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -50,7 +52,7 @@ class GeneralLightningRoutes { const statistics = await statisticsApi.$getLatestStatistics(); res.json(statistics); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } diff --git a/backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts index 9d6373845..9ca2fd1c3 100644 --- a/backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express'; import nodesApi from './nodes.api'; import DB from '../../database'; import { INodesRanking } from '../../mempool.interfaces'; +import { handleError } from '../../utils/api'; class NodesRoutes { constructor() { } @@ -31,7 +32,7 @@ class NodesRoutes { const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search); res.json(nodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -181,13 +182,13 @@ class NodesRoutes { } } catch (e) {} } - + res.header('Pragma', 'public'); res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(nodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -195,7 +196,7 @@ class NodesRoutes { try { const node = await nodesApi.$getNode(req.params.public_key); if (!node) { - res.status(404).send('Node not found'); + handleError(req, res, 404, 'Node not found'); return; } res.header('Pragma', 'public'); @@ -203,7 +204,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(node); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -215,7 +216,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(statistics); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -223,7 +224,7 @@ class NodesRoutes { try { const node = await nodesApi.$getFeeHistogram(req.params.public_key); if (!node) { - res.status(404).send('Node not found'); + handleError(req, res, 404, 'Node not found'); return; } res.header('Pragma', 'public'); @@ -231,7 +232,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(node); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -247,7 +248,7 @@ class NodesRoutes { topByChannels: topChannelsNodes, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -259,7 +260,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(topCapacityNodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -271,7 +272,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(topCapacityNodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -283,7 +284,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(topCapacityNodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -295,7 +296,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.json(nodesPerAs); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -307,7 +308,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.json(worldNodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -322,7 +323,7 @@ class NodesRoutes { ); if (country.length === 0) { - res.status(404).send(`This country does not exist or does not host any lightning nodes on clearnet`); + handleError(req, res, 404, `This country does not exist or does not host any lightning nodes on clearnet`); return; } @@ -335,7 +336,7 @@ class NodesRoutes { nodes: nodes, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -349,7 +350,7 @@ class NodesRoutes { ); if (isp.length === 0) { - res.status(404).send(`This ISP does not exist or does not host any lightning nodes on clearnet`); + handleError(req, res, 404, `This ISP does not exist or does not host any lightning nodes on clearnet`); return; } @@ -362,7 +363,7 @@ class NodesRoutes { nodes: nodes, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -374,7 +375,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.json(nodesPerAs); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } diff --git a/backend/src/api/liquid/liquid.routes.ts b/backend/src/api/liquid/liquid.routes.ts index 9ea61ca31..9dafd0def 100644 --- a/backend/src/api/liquid/liquid.routes.ts +++ b/backend/src/api/liquid/liquid.routes.ts @@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express'; import config from '../../config'; import elementsParser from './elements-parser'; import icons from './icons'; +import { handleError } from '../../utils/api'; class LiquidRoutes { public initRoutes(app: Application) { @@ -42,7 +43,7 @@ class LiquidRoutes { res.setHeader('content-length', result.length); res.send(result); } else { - res.status(404).send('Asset icon not found'); + handleError(req, res, 404, 'Asset icon not found'); } } @@ -51,7 +52,7 @@ class LiquidRoutes { if (result) { res.json(result); } else { - res.status(404).send('Asset icons not found'); + handleError(req, res, 404, 'Asset icons not found'); } } @@ -82,7 +83,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); res.json(pegs); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -94,7 +95,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); res.json(reserves); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -106,7 +107,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(currentSupply); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -118,7 +119,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(currentReserves); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -130,7 +131,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(auditStatus); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -142,7 +143,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationAddresses); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -154,7 +155,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationAddresses); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -166,7 +167,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -178,7 +179,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(expiredUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -190,7 +191,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -202,7 +203,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(emergencySpentUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -214,7 +215,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(emergencySpentUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -226,7 +227,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(recentPegs); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -238,7 +239,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(pegsVolume); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -250,7 +251,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(pegsCount); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index 5d9dcf8f4..6e547e653 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -369,7 +369,7 @@ class MempoolBlocks { const lastBlockIndex = blocks.length - 1; let hasBlockStack = blocks.length >= 8; let stackWeight; - let feeStatsCalculator: OnlineFeeStatsCalculator | void; + let feeStatsCalculator: OnlineFeeStatsCalculator | null = null; if (hasBlockStack) { if (blockWeights && blockWeights[7] !== null) { stackWeight = blockWeights[7]; @@ -380,28 +380,36 @@ class MempoolBlocks { feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]); } + const ancestors: Ancestor[] = []; + const descendants: Ancestor[] = []; + let ancestor: MempoolTransactionExtended for (const cluster of clusters) { for (const memberTxid of cluster) { const mempoolTx = mempool[memberTxid]; if (mempoolTx) { - const ancestors: Ancestor[] = []; - const descendants: Ancestor[] = []; + // ugly micro-optimization to avoid allocating new arrays + ancestors.length = 0; + descendants.length = 0; let matched = false; cluster.forEach(txid => { + ancestor = mempool[txid]; if (txid === memberTxid) { matched = true; } else { - if (!mempool[txid]) { + if (!ancestor) { console.log('txid missing from mempool! ', txid, candidates?.txs[txid]); + return; } const relative = { txid: txid, - fee: mempool[txid].fee, - weight: (mempool[txid].adjustedVsize * 4), + fee: ancestor.fee, + weight: (ancestor.adjustedVsize * 4), }; if (matched) { descendants.push(relative); - mempoolTx.lastBoosted = Math.max(mempoolTx.lastBoosted || 0, mempool[txid].firstSeen || 0); + if (!mempoolTx.lastBoosted || (ancestor.firstSeen && ancestor.firstSeen > mempoolTx.lastBoosted)) { + mempoolTx.lastBoosted = ancestor.firstSeen; + } } else { ancestors.push(relative); } @@ -410,7 +418,20 @@ class MempoolBlocks { if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) { mempoolTx.cpfpDirty = true; } - Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true}); + // ugly micro-optimization to avoid allocating new arrays or objects + if (mempoolTx.ancestors) { + mempoolTx.ancestors.length = 0; + } else { + mempoolTx.ancestors = []; + } + if (mempoolTx.descendants) { + mempoolTx.descendants.length = 0; + } else { + mempoolTx.descendants = []; + } + mempoolTx.ancestors.push(...ancestors); + mempoolTx.descendants.push(...descendants); + mempoolTx.cpfpChecked = true; } } } @@ -420,7 +441,10 @@ class MempoolBlocks { const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2; // update this thread's mempool with the results let mempoolTx: MempoolTransactionExtended; - const mempoolBlocks: MempoolBlockWithTransactions[] = blocks.map((block, blockIndex) => { + let acceleration: Acceleration; + const mempoolBlocks: MempoolBlockWithTransactions[] = []; + for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) { + const block = blocks[blockIndex]; let totalSize = 0; let totalVsize = 0; let totalWeight = 0; @@ -436,7 +460,8 @@ class MempoolBlocks { } } - for (const txid of block) { + for (let i = 0; i < block.length; i++) { + const txid = block[i]; if (txid) { mempoolTx = mempool[txid]; // save position in projected blocks @@ -445,30 +470,37 @@ class MempoolBlocks { vsize: totalVsize + (mempoolTx.vsize / 2), }; - const acceleration = accelerations[txid]; - if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { - if (!mempoolTx.acceleration) { - mempoolTx.cpfpDirty = true; - } - mempoolTx.acceleration = true; - mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools; - mempoolTx.acceleratedAt = acceleration?.added; - mempoolTx.feeDelta = acceleration?.feeDelta; - for (const ancestor of mempoolTx.ancestors || []) { - if (!mempool[ancestor.txid].acceleration) { - mempool[ancestor.txid].cpfpDirty = true; + if (txid in accelerations) { + acceleration = accelerations[txid]; + if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { + if (!mempoolTx.acceleration) { + mempoolTx.cpfpDirty = true; + } + mempoolTx.acceleration = true; + mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools; + mempoolTx.acceleratedAt = acceleration?.added; + mempoolTx.feeDelta = acceleration?.feeDelta; + for (const ancestor of mempoolTx.ancestors || []) { + if (!mempool[ancestor.txid].acceleration) { + mempool[ancestor.txid].cpfpDirty = true; + } + mempool[ancestor.txid].acceleration = true; + mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy; + mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt; + mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta; + isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy; + } + } else { + if (mempoolTx.acceleration) { + mempoolTx.cpfpDirty = true; + delete mempoolTx.acceleration; } - mempool[ancestor.txid].acceleration = true; - mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy; - mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt; - mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta; - isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy; } } else { if (mempoolTx.acceleration) { mempoolTx.cpfpDirty = true; + delete mempoolTx.acceleration; } - delete mempoolTx.acceleration; } // online calculation of stack-of-blocks fee stats @@ -486,7 +518,7 @@ class MempoolBlocks { } } } - return this.dataToMempoolBlocks( + mempoolBlocks[blockIndex] = this.dataToMempoolBlocks( block, transactions, totalSize, @@ -494,7 +526,7 @@ class MempoolBlocks { totalFees, (hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined, ); - }); + }; if (saveResults) { const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks); diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index 1f55179fb..87e7f10cd 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -10,6 +10,7 @@ import bitcoinClient from './bitcoin/bitcoin-client'; import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; import rbfCache from './rbf-cache'; import { Acceleration } from './services/acceleration'; +import accelerationApi from './services/acceleration'; import redisCache from './redis-cache'; import blocks from './blocks'; @@ -19,12 +20,13 @@ class Mempool { private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {}; private mempoolCandidates: { [txid: string ]: boolean } = {}; private spendMap = new Map(); + private recentlyDeleted: MempoolTransactionExtended[][] = []; // buffer of transactions deleted in recent mempool updates private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0, maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 }; private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[], - deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined; + deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void) | undefined; private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[], - deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise) | undefined; + deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise) | undefined; private accelerations: { [txId: string]: Acceleration } = {}; private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {}; @@ -74,12 +76,12 @@ class Mempool { } public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, - newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void { + newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void): void { this.mempoolChangedCallback = fn; } public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number, - newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], + newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise): void { this.$asyncMempoolChangedCallback = fn; } @@ -206,7 +208,7 @@ class Mempool { return txTimes; } - public async $updateMempool(transactions: string[], accelerations: Acceleration[] | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise { + public async $updateMempool(transactions: string[], accelerations: Record | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise { logger.debug(`Updating mempool...`); // warn if this run stalls the main loop for more than 2 minutes @@ -353,7 +355,7 @@ class Mempool { const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx)); this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6); - const accelerationDelta = accelerations != null ? await this.$updateAccelerations(accelerations) : []; + const accelerationDelta = accelerations != null ? await this.updateAccelerations(accelerations) : []; if (accelerationDelta.length) { hasChange = true; } @@ -362,12 +364,15 @@ class Mempool { const candidatesChanged = candidates?.added?.length || candidates?.removed?.length; - if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) { - this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta); + this.recentlyDeleted.unshift(deletedTransactions); + this.recentlyDeleted.length = Math.min(this.recentlyDeleted.length, 10); // truncate to the last 10 mempool updates + + if (this.mempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length)) { + this.mempoolChangedCallback(this.mempoolCache, newTransactions, this.recentlyDeleted, accelerationDelta); } - if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length || candidatesChanged)) { + if (this.$asyncMempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length || candidatesChanged)) { this.updateTimerProgress(timer, 'running async mempool callback'); - await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta, candidates); + await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, this.recentlyDeleted, accelerationDelta, candidates); this.updateTimerProgress(timer, 'completed async mempool callback'); } @@ -395,58 +400,11 @@ class Mempool { return this.accelerations; } - public $updateAccelerations(newAccelerations: Acceleration[]): string[] { + public updateAccelerations(newAccelerationMap: Record): string[] { try { - const changed: string[] = []; - - const newAccelerationMap: { [txid: string]: Acceleration } = {}; - for (const acceleration of newAccelerations) { - // skip transactions we don't know about - if (!this.mempoolCache[acceleration.txid]) { - continue; - } - newAccelerationMap[acceleration.txid] = acceleration; - if (this.accelerations[acceleration.txid] == null) { - // new acceleration - changed.push(acceleration.txid); - } else { - if (this.accelerations[acceleration.txid].feeDelta !== acceleration.feeDelta) { - // feeDelta changed - changed.push(acceleration.txid); - } else if (this.accelerations[acceleration.txid].pools?.length) { - let poolsChanged = false; - const pools = new Set(); - this.accelerations[acceleration.txid].pools.forEach(pool => { - pools.add(pool); - }); - acceleration.pools.forEach(pool => { - if (!pools.has(pool)) { - poolsChanged = true; - } else { - pools.delete(pool); - } - }); - if (pools.size > 0) { - poolsChanged = true; - } - if (poolsChanged) { - // pools changed - changed.push(acceleration.txid); - } - } - } - } - - for (const oldTxid of Object.keys(this.accelerations)) { - if (!newAccelerationMap[oldTxid]) { - // removed - changed.push(oldTxid); - } - } - + const accelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, newAccelerationMap); this.accelerations = newAccelerationMap; - - return changed; + return accelerationDelta; } catch (e: any) { logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e)); return []; @@ -541,16 +499,7 @@ class Mempool { } } - public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void { - for (const rbfTransaction in rbfTransactions) { - if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) { - // Store replaced transactions - rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]); - } - } - } - - public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void { + public handleRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void { for (const rbfTransaction in rbfTransactions) { if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) { // Store replaced transactions diff --git a/backend/src/api/mining/mining-routes.ts b/backend/src/api/mining/mining-routes.ts index 8f8bbac82..9af43c087 100644 --- a/backend/src/api/mining/mining-routes.ts +++ b/backend/src/api/mining/mining-routes.ts @@ -10,6 +10,7 @@ import mining from "./mining"; import PricesRepository from '../../repositories/PricesRepository'; import AccelerationRepository from '../../repositories/AccelerationRepository'; import accelerationApi from '../services/acceleration'; +import { handleError } from '../../utils/api'; class MiningRoutes { public initRoutes(app: Application) { @@ -53,12 +54,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Prices are not available on testnets.'); + handleError(req, res, 400, 'Prices are not available on testnets.'); return; } const timestamp = parseInt(req.query.timestamp as string, 10) || 0; const currency = req.query.currency as string; - + let response; if (timestamp && currency) { response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency); @@ -71,7 +72,7 @@ class MiningRoutes { } res.status(200).send(response); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -84,9 +85,9 @@ class MiningRoutes { res.json(stats); } catch (e) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { - res.status(404).send(e.message); + handleError(req, res, 404, e.message); } else { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } @@ -103,9 +104,9 @@ class MiningRoutes { res.json(poolBlocks); } catch (e) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { - res.status(404).send(e.message); + handleError(req, res, 404, e.message); } else { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } @@ -129,7 +130,7 @@ class MiningRoutes { res.json(pools); } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -143,7 +144,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(stats); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -157,7 +158,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.json(hashrates); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -172,9 +173,9 @@ class MiningRoutes { res.json(hashrates); } catch (e) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { - res.status(404).send(e.message); + handleError(req, res, 404, e.message); } else { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } @@ -182,7 +183,7 @@ class MiningRoutes { private async $getHistoricalHashrate(req: Request, res: Response) { let currentHashrate = 0, currentDifficulty = 0; try { - currentHashrate = await bitcoinClient.getNetworkHashPs(); + currentHashrate = await bitcoinClient.getNetworkHashPs(1008); currentDifficulty = await bitcoinClient.getDifficulty(); } catch (e) { logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty'); @@ -203,7 +204,7 @@ class MiningRoutes { currentDifficulty: currentDifficulty, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -217,7 +218,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockFees); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -235,7 +236,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockFees); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -249,7 +250,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockRewards); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -263,7 +264,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockFeeRates); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -281,7 +282,7 @@ class MiningRoutes { weights: blockWeights }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -293,7 +294,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment])); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -317,7 +318,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate])); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -326,7 +327,7 @@ class MiningRoutes { const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash); if (!audit) { - res.status(204).send(`This block has not been audited.`); + handleError(req, res, 204, `This block has not been audited.`); return; } @@ -335,7 +336,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.json(audit); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -358,7 +359,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -371,7 +372,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -384,7 +385,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.json(audit || 'null'); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -394,12 +395,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -409,13 +410,13 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -425,12 +426,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -440,12 +441,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } res.status(200).send(await AccelerationRepository.$getAccelerationTotals(req.query.pool, req.query.interval)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -455,12 +456,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } - res.status(200).send(accelerationApi.accelerations || []); + res.status(200).send(Object.values(accelerationApi.getAccelerations() || {})); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -472,7 +473,7 @@ class MiningRoutes { accelerationApi.accelerationRequested(req.params.txid); res.status(200).send(); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index a087abbe0..944ad790e 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -44,6 +44,22 @@ interface CacheEvent { value?: any, } +/** + * Singleton for tracking RBF trees + * + * Maintains a set of RBF trees, where each tree represents a sequence of + * consecutive RBF replacements. + * + * Trees are identified by the txid of the root transaction. + * + * To maintain consistency, the following invariants must be upheld: + * - Symmetry: replacedBy(A) = B <=> A in replaces(B) + * - Unique id: treeMap(treeMap(X)) = treeMap(X) + * - Unique tree: A in replaces(B) => treeMap(A) == treeMap(B) + * - Existence: X in treeMap => treeMap(X) in rbfTrees + * - Completeness: X in replacedBy => X in treeMap, Y in replaces => Y in treeMap + */ + class RbfCache { private replacedBy: Map = new Map(); private replaces: Map = new Map(); @@ -61,6 +77,10 @@ class RbfCache { setInterval(this.cleanup.bind(this), 1000 * 60 * 10); } + /** + * Low level cache operations + */ + private addTx(txid: string, tx: MempoolTransactionExtended): void { this.txs.set(txid, tx); this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid }); @@ -92,6 +112,12 @@ class RbfCache { this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid }); } + /** + * Basic data structure operations + * must uphold tree invariants + */ + + public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void { if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) { return; @@ -114,6 +140,10 @@ class RbfCache { if (!replacedTx.rbf) { txFullRbf = true; } + if (this.replacedBy.has(replacedTx.txid)) { + // should never happen + continue; + } this.replacedBy.set(replacedTx.txid, newTx.txid); if (this.treeMap.has(replacedTx.txid)) { const treeId = this.treeMap.get(replacedTx.txid); @@ -140,18 +170,47 @@ class RbfCache { } } newTx.fullRbf = txFullRbf; - const treeId = replacedTrees[0].tx.txid; const newTree = { tx: newTx, time: newTime, fullRbf: treeFullRbf, replaces: replacedTrees }; - this.addTree(treeId, newTree); - this.updateTreeMap(treeId, newTree); + this.addTree(newTree.tx.txid, newTree); + this.updateTreeMap(newTree.tx.txid, newTree); this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid)); } + public mined(txid): void { + if (!this.txs.has(txid)) { + return; + } + const treeId = this.treeMap.get(txid); + if (treeId && this.rbfTrees.has(treeId)) { + const tree = this.rbfTrees.get(treeId); + if (tree) { + this.setTreeMined(tree, txid); + tree.mined = true; + this.dirtyTrees.add(treeId); + this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId }); + } + } + this.evict(txid); + } + + // flag a transaction as removed from the mempool + public evict(txid: string, fast: boolean = false): void { + this.evictionCount++; + if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) { + const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours + this.addExpiration(txid, expiryTime); + } + } + + /** + * Read-only public interface + */ + public has(txId: string): boolean { return this.txs.has(txId); } @@ -232,32 +291,6 @@ class RbfCache { return changes; } - public mined(txid): void { - if (!this.txs.has(txid)) { - return; - } - const treeId = this.treeMap.get(txid); - if (treeId && this.rbfTrees.has(treeId)) { - const tree = this.rbfTrees.get(treeId); - if (tree) { - this.setTreeMined(tree, txid); - tree.mined = true; - this.dirtyTrees.add(treeId); - this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId }); - } - } - this.evict(txid); - } - - // flag a transaction as removed from the mempool - public evict(txid: string, fast: boolean = false): void { - this.evictionCount++; - if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) { - const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours - this.addExpiration(txid, expiryTime); - } - } - // is the transaction involved in a full rbf replacement? public isFullRbf(txid: string): boolean { const treeId = this.treeMap.get(txid); @@ -271,6 +304,10 @@ class RbfCache { return tree?.fullRbf; } + /** + * Cache maintenance & utility functions + */ + private cleanup(): void { const now = Date.now(); for (const txid of this.expiring.keys()) { @@ -299,10 +336,6 @@ class RbfCache { for (const tx of (replaces || [])) { // recursively remove prior versions from the cache this.replacedBy.delete(tx); - // if this is the id of a tree, remove that too - if (this.treeMap.get(tx) === tx) { - this.removeTree(tx); - } this.remove(tx); } } @@ -370,14 +403,21 @@ class RbfCache { }; } - public async load({ txs, trees, expiring, mempool }): Promise { + public async load({ txs, trees, expiring, mempool, spendMap }): Promise { try { txs.forEach(txEntry => { this.txs.set(txEntry.value.txid, txEntry.value); }); this.staleCount = 0; - for (const deflatedTree of trees) { - await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs); + for (const deflatedTree of trees.sort((a, b) => Object.keys(b).length - Object.keys(a).length)) { + const tree = await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs); + if (tree) { + this.addTree(tree.tx.txid, tree); + this.updateTreeMap(tree.tx.txid, tree); + if (tree.mined) { + this.evict(tree.tx.txid); + } + } } expiring.forEach(expiringEntry => { if (this.txs.has(expiringEntry.key)) { @@ -385,6 +425,31 @@ class RbfCache { } }); this.staleCount = 0; + + // connect cached trees to current mempool transactions + const conflicts: Record }> = {}; + for (const tree of this.rbfTrees.values()) { + const tx = this.getTx(tree.tx.txid); + if (!tx || tree.mined) { + continue; + } + for (const vin of tx.vin) { + const conflict = spendMap.get(`${vin.txid}:${vin.vout}`); + if (conflict && conflict.txid !== tx.txid) { + if (!conflicts[conflict.txid]) { + conflicts[conflict.txid] = { + replacedBy: conflict, + replaces: new Set(), + }; + } + conflicts[conflict.txid].replaces.add(tx); + } + } + } + for (const { replacedBy, replaces } of Object.values(conflicts)) { + this.add([...replaces.values()], replacedBy); + } + await this.checkTrees(); logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`); this.cleanup(); @@ -426,6 +491,12 @@ class RbfCache { return; } + // if this tx is already in the cache, return early + if (this.treeMap.has(txid)) { + this.removeTree(deflated.key); + return; + } + // recursively reconstruct child trees for (const childId of treeInfo.replaces) { const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined); @@ -457,10 +528,6 @@ class RbfCache { fullRbf: treeInfo.fullRbf, replaces, }; - this.treeMap.set(txid, root); - if (root === txid) { - this.addTree(root, tree); - } return tree; } @@ -511,6 +578,7 @@ class RbfCache { processTxs(txs); } + // evict missing transactions for (const txid of txids) { if (!found[txid]) { this.evict(txid, false); diff --git a/backend/src/api/redis-cache.ts b/backend/src/api/redis-cache.ts index cbfa2f18b..1caade15b 100644 --- a/backend/src/api/redis-cache.ts +++ b/backend/src/api/redis-cache.ts @@ -365,6 +365,7 @@ class RedisCache { trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }), expiring: rbfExpirations, mempool: memPool.getMempool(), + spendMap: memPool.getSpendMap(), }); } diff --git a/backend/src/api/services/acceleration.ts b/backend/src/api/services/acceleration.ts index 88289382b..f625b7f15 100644 --- a/backend/src/api/services/acceleration.ts +++ b/backend/src/api/services/acceleration.ts @@ -1,7 +1,10 @@ +import { WebSocket } from 'ws'; import config from '../../config'; import logger from '../../logger'; import { BlockExtended } from '../../mempool.interfaces'; import axios from 'axios'; +import mempool from '../mempool'; +import websocketHandler from '../websocket-handler'; type MyAccelerationStatus = 'requested' | 'accelerating' | 'done'; @@ -37,14 +40,20 @@ export interface AccelerationHistory { }; class AccelerationApi { + private ws: WebSocket | null = null; + private useWebsocket: boolean = config.MEMPOOL.OFFICIAL && config.MEMPOOL_SERVICES.ACCELERATIONS; + private startedWebsocketLoop: boolean = false; + private websocketConnected: boolean = false; private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS; private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations'); - private _accelerations: Acceleration[] | null = null; + private _accelerations: Record = {}; private lastPoll = 0; private forcePoll = false; private myAccelerations: Record = {}; - public get accelerations(): Acceleration[] | null { + public constructor() {} + + public getAccelerations(): Record { return this._accelerations; } @@ -72,11 +81,18 @@ class AccelerationApi { } } - public async $updateAccelerations(): Promise { + public async $updateAccelerations(): Promise | null> { + if (this.useWebsocket && this.websocketConnected) { + return this._accelerations; + } if (!this.onDemandPollingEnabled) { const accelerations = await this.$fetchAccelerations(); if (accelerations) { - this._accelerations = accelerations; + const latestAccelerations = {}; + for (const acc of accelerations) { + latestAccelerations[acc.txid] = acc; + } + this._accelerations = latestAccelerations; return this._accelerations; } } else { @@ -85,7 +101,7 @@ class AccelerationApi { return null; } - private async $updateAccelerationsOnDemand(): Promise { + private async $updateAccelerationsOnDemand(): Promise | null> { const shouldUpdate = this.forcePoll || this.countMyAccelerationsWithStatus('requested') > 0 || (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000))); @@ -120,7 +136,11 @@ class AccelerationApi { } } - this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]; + const latestAccelerations = {}; + for (const acc of Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]) { + latestAccelerations[acc.txid] = acc; + } + this._accelerations = latestAccelerations; return this._accelerations; } @@ -152,6 +172,110 @@ class AccelerationApi { } return anyAccelerated; } + + // get a list of accelerations that have changed between two sets of accelerations + public getAccelerationDelta(oldAccelerationMap: Record, newAccelerationMap: Record): string[] { + const changed: string[] = []; + const mempoolCache = mempool.getMempool(); + + for (const acceleration of Object.values(newAccelerationMap)) { + // skip transactions we don't know about + if (!mempoolCache[acceleration.txid]) { + continue; + } + if (oldAccelerationMap[acceleration.txid] == null) { + // new acceleration + changed.push(acceleration.txid); + } else { + if (oldAccelerationMap[acceleration.txid].feeDelta !== acceleration.feeDelta) { + // feeDelta changed + changed.push(acceleration.txid); + } else if (oldAccelerationMap[acceleration.txid].pools?.length) { + let poolsChanged = false; + const pools = new Set(); + oldAccelerationMap[acceleration.txid].pools.forEach(pool => { + pools.add(pool); + }); + acceleration.pools.forEach(pool => { + if (!pools.has(pool)) { + poolsChanged = true; + } else { + pools.delete(pool); + } + }); + if (pools.size > 0) { + poolsChanged = true; + } + if (poolsChanged) { + // pools changed + changed.push(acceleration.txid); + } + } + } + } + + for (const oldTxid of Object.keys(oldAccelerationMap)) { + if (!newAccelerationMap[oldTxid]) { + // removed + changed.push(oldTxid); + } + } + + return changed; + } + + private handleWebsocketMessage(msg: any): void { + if (msg?.accelerations !== null) { + const latestAccelerations = {}; + for (const acc of msg?.accelerations || []) { + latestAccelerations[acc.txid] = acc; + } + this._accelerations = latestAccelerations; + websocketHandler.handleAccelerationsChanged(this._accelerations); + } + } + + public async connectWebsocket(): Promise { + if (this.startedWebsocketLoop) { + return; + } + while (this.useWebsocket) { + this.startedWebsocketLoop = true; + if (!this.ws) { + this.ws = new WebSocket(`${config.MEMPOOL_SERVICES.API.replace('https://', 'ws://').replace('http://', 'ws://')}/accelerator/ws`); + this.websocketConnected = true; + + this.ws.on('open', () => { + logger.info('Acceleration websocket opened'); + this.ws?.send(JSON.stringify({ + 'watch-accelerations': true + })); + }); + + this.ws.on('error', (error) => { + logger.err('Acceleration websocket error: ' + error); + this.ws = null; + this.websocketConnected = false; + }); + + this.ws.on('close', () => { + logger.info('Acceleration websocket closed'); + this.ws = null; + this.websocketConnected = false; + }); + + this.ws.on('message', (data, isBinary) => { + try { + const parsedMsg = JSON.parse((isBinary ? data : data.toString()) as string); + this.handleWebsocketMessage(parsedMsg); + } catch (e) { + logger.warn('Failed to parse acceleration websocket message: ' + (e instanceof Error ? e.message : e)); + } + }); + } + await new Promise(resolve => setTimeout(resolve, 5000)); + } + } } export default new AccelerationApi(); \ No newline at end of file diff --git a/backend/src/api/services/services-routes.ts b/backend/src/api/services/services-routes.ts new file mode 100644 index 000000000..cff163174 --- /dev/null +++ b/backend/src/api/services/services-routes.ts @@ -0,0 +1,26 @@ +import { Application, Request, Response } from 'express'; +import config from '../../config'; +import WalletApi from './wallets'; + +class ServicesRoutes { + public initRoutes(app: Application): void { + app + .get(config.MEMPOOL.API_URL_PREFIX + 'wallet/:walletId', this.$getWallet) + ; + } + + private async $getWallet(req: Request, res: Response): Promise { + try { + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 5).toUTCString()); + const walletId = req.params.walletId; + const wallet = await WalletApi.getWallet(walletId); + res.status(200).send(wallet); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } +} + +export default new ServicesRoutes(); diff --git a/backend/src/api/services/wallets.ts b/backend/src/api/services/wallets.ts new file mode 100644 index 000000000..dd4d7ebc9 --- /dev/null +++ b/backend/src/api/services/wallets.ts @@ -0,0 +1,153 @@ +import config from '../../config'; +import logger from '../../logger'; +import { IEsploraApi } from '../bitcoin/esplora-api.interface'; +import bitcoinApi from '../bitcoin/bitcoin-api-factory'; +import axios from 'axios'; +import { TransactionExtended } from '../../mempool.interfaces'; + +interface WalletAddress { + address: string; + active: boolean; + stats: { + funded_txo_count: number; + funded_txo_sum: number; + spent_txo_count: number; + spent_txo_sum: number; + tx_count: number; + }; + transactions: IEsploraApi.AddressTxSummary[]; + lastSync: number; +} + +interface Wallet { + name: string; + addresses: Record; + lastPoll: number; +} + +const POLL_FREQUENCY = 5 * 60 * 1000; // 5 minutes + +class WalletApi { + private wallets: Record = {}; + private syncing = false; + + constructor() { + this.wallets = config.WALLETS.ENABLED ? (config.WALLETS.WALLETS as string[]).reduce((acc, wallet) => { + acc[wallet] = { name: wallet, addresses: {}, lastPoll: 0 }; + return acc; + }, {} as Record) : {}; + } + + public getWallet(wallet: string): Record { + return this.wallets?.[wallet]?.addresses || {}; + } + + // resync wallet addresses from the services backend + async $syncWallets(): Promise { + if (!config.WALLETS.ENABLED || this.syncing) { + return; + } + this.syncing = true; + for (const walletKey of Object.keys(this.wallets)) { + const wallet = this.wallets[walletKey]; + if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) { + try { + const response = await axios.get(config.MEMPOOL_SERVICES.API + `/wallets/${wallet.name}`); + const addresses: Record = response.data; + const addressList: WalletAddress[] = Object.values(addresses); + // sync all current addresses + for (const address of addressList) { + await this.$syncWalletAddress(wallet, address); + } + // remove old addresses + for (const address of Object.keys(wallet.addresses)) { + if (!addresses[address]) { + delete wallet.addresses[address]; + } + } + wallet.lastPoll = Date.now(); + logger.debug(`Synced ${Object.keys(wallet.addresses).length} addresses for wallet ${wallet.name}`); + } catch (e) { + logger.err(`Error syncing wallet ${wallet.name}: ${(e instanceof Error ? e.message : e)}`); + } + } + } + this.syncing = false; + } + + // resync address transactions from esplora + async $syncWalletAddress(wallet: Wallet, address: WalletAddress): Promise { + // fetch full transaction data if the address is new or still active and hasn't been synced in the last hour + const refreshTransactions = !wallet.addresses[address.address] || (address.active && (Date.now() - wallet.addresses[address.address].lastSync) > 60 * 60 * 1000); + if (refreshTransactions) { + try { + const summary = await bitcoinApi.$getAddressTransactionSummary(address.address); + const addressInfo = await bitcoinApi.$getAddress(address.address); + const walletAddress: WalletAddress = { + address: address.address, + active: address.active, + transactions: summary, + stats: addressInfo.chain_stats, + lastSync: Date.now(), + }; + wallet.addresses[address.address] = walletAddress; + } catch (e) { + logger.err(`Error syncing wallet address ${address.address}: ${(e instanceof Error ? e.message : e)}`); + } + } + } + + // check a new block for transactions that affect wallet address balances, and add relevant transactions to wallets + processBlock(block: IEsploraApi.Block, blockTxs: TransactionExtended[]): Record { + const walletTransactions: Record = {}; + for (const walletKey of Object.keys(this.wallets)) { + const wallet = this.wallets[walletKey]; + walletTransactions[walletKey] = []; + for (const tx of blockTxs) { + const funded: Record = {}; + const spent: Record = {}; + const fundedCount: Record = {}; + const spentCount: Record = {}; + let anyMatch = false; + for (const vin of tx.vin) { + const address = vin.prevout?.scriptpubkey_address; + if (address && wallet.addresses[address]) { + anyMatch = true; + spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0); + spentCount[address] = (spentCount[address] ?? 0) + 1; + } + } + for (const vout of tx.vout) { + const address = vout.scriptpubkey_address; + if (address && wallet.addresses[address]) { + anyMatch = true; + funded[address] = (funded[address] ?? 0) + (vout.value ?? 0); + fundedCount[address] = (fundedCount[address] ?? 0) + 1; + } + } + for (const address of Object.keys({ ...funded, ...spent })) { + // update address stats + wallet.addresses[address].stats.tx_count++; + wallet.addresses[address].stats.funded_txo_count += fundedCount[address] || 0; + wallet.addresses[address].stats.spent_txo_count += spentCount[address] || 0; + wallet.addresses[address].stats.funded_txo_sum += funded[address] || 0; + wallet.addresses[address].stats.spent_txo_sum += spent[address] || 0; + // add tx to summary + const txSummary: IEsploraApi.AddressTxSummary = { + txid: tx.txid, + value: (funded[address] ?? 0) - (spent[address] ?? 0), + height: block.height, + time: block.timestamp, + }; + wallet.addresses[address].transactions?.push(txSummary); + } + if (anyMatch) { + walletTransactions[walletKey].push(tx); + } + } + } + return walletTransactions; + } +} + +export default new WalletApi(); \ No newline at end of file diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts index 15d3e7110..28fa72bba 100644 --- a/backend/src/api/transaction-utils.ts +++ b/backend/src/api/transaction-utils.ts @@ -121,6 +121,7 @@ class TransactionUtils { const adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor const feePerVbytes = (transaction.fee || 0) / fractionalVsize; const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize; + const effectiveFeePerVsize = transaction['effectiveFeePerVsize'] || adjustedFeePerVsize || feePerVbytes; const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, { order: this.txidToOrdering(transaction.txid), vsize, @@ -128,7 +129,7 @@ class TransactionUtils { sigops, feePerVsize: feePerVbytes, adjustedFeePerVsize: adjustedFeePerVsize, - effectiveFeePerVsize: adjustedFeePerVsize, + effectiveFeePerVsize: effectiveFeePerVsize, }); if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) { transactionExtended.firstSeen = Math.round((Date.now() / 1000)); diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 79a783f88..13e27c360 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -16,16 +16,19 @@ import transactionUtils from './transaction-utils'; import rbfCache, { ReplacementInfo } from './rbf-cache'; import difficultyAdjustment from './difficulty-adjustment'; import feeApi from './fee-api'; +import BlocksRepository from '../repositories/BlocksRepository'; import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository'; import Audit from './audit'; import priceUpdater from '../tasks/price-updater'; import { ApiPrice } from '../repositories/PricesRepository'; +import { Acceleration } from './services/acceleration'; import accelerationApi from './services/acceleration'; import mempool from './mempool'; import statistics from './statistics/statistics'; import accelerationRepository from '../repositories/AccelerationRepository'; import bitcoinApi from './bitcoin/bitcoin-api-factory'; +import walletApi from './services/wallets'; interface AddressTransactions { mempool: MempoolTransactionExtended[], @@ -34,6 +37,7 @@ interface AddressTransactions { } import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; import { calculateMempoolTxCpfp } from './cpfp'; +import { getRecentFirstSeen } from '../utils/file-read'; // valid 'want' subscriptions const wantable = [ @@ -57,6 +61,8 @@ class WebsocketHandler { private lastRbfSummary: ReplacementInfo[] | null = null; private mempoolSequence: number = 0; + private accelerations: Record = {}; + constructor() { } addWebsocketServer(wss: WebSocket.Server) { @@ -305,6 +311,14 @@ class WebsocketHandler { } } + if (parsedMessage && parsedMessage['track-wallet']) { + if (parsedMessage['track-wallet'] === 'stop') { + client['track-wallet'] = null; + } else { + client['track-wallet'] = parsedMessage['track-wallet']; + } + } + if (parsedMessage && parsedMessage['track-asset']) { if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) { client['track-asset'] = parsedMessage['track-asset']; @@ -484,6 +498,42 @@ class WebsocketHandler { } } + handleAccelerationsChanged(accelerations: Record): void { + if (!this.webSocketServers.length) { + throw new Error('No WebSocket.Server has been set'); + } + + const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations); + this.accelerations = accelerations; + + if (!websocketAccelerationDelta.length) { + return; + } + + // pre-compute acceleration delta + const accelerationUpdate = { + added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null), + removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]), + }; + + try { + const response = JSON.stringify({ + accelerations: accelerationUpdate, + }); + + for (const server of this.webSocketServers) { + server.clients.forEach((client) => { + if (client.readyState !== WebSocket.OPEN) { + return; + } + client.send(response); + }); + } + } catch (e) { + logger.debug(`Error sending acceleration update to websocket clients: ${e}`); + } + } + handleReorg(): void { if (!this.webSocketServers.length) { throw new Error('No WebSocket.Server have been set'); @@ -520,8 +570,17 @@ class WebsocketHandler { } } + /** + * + * @param newMempool + * @param mempoolSize + * @param newTransactions array of transactions added this mempool update. + * @param recentlyDeletedTransactions array of arrays of transactions removed in the last N mempool updates, most recent first. + * @param accelerationDelta + * @param candidates + */ async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number, - newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], + newTransactions: MempoolTransactionExtended[], recentlyDeletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates): Promise { if (!this.webSocketServers.length) { throw new Error('No WebSocket.Server have been set'); @@ -529,6 +588,8 @@ class WebsocketHandler { this.printLogs(); + const deletedTransactions = recentlyDeletedTransactions.length ? recentlyDeletedTransactions[0] : []; + const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool); let added = newTransactions; let removed = deletedTransactions; @@ -547,9 +608,9 @@ class WebsocketHandler { const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); const mempoolInfo = memPool.getMempoolInfo(); const vBytesPerSecond = memPool.getVBytesPerSecond(); - const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions); + const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat()); const da = difficultyAdjustment.getDifficultyAdjustment(); - const accelerations = memPool.getAccelerations(); + const accelerations = accelerationApi.getAccelerations(); memPool.handleRbfTransactions(rbfTransactions); const rbfChanges = rbfCache.getRbfChanges(); let rbfReplacements; @@ -578,7 +639,7 @@ class WebsocketHandler { const replacedTransactions: { replaced: string, by: TransactionExtended }[] = []; for (const tx of newTransactions) { if (rbfTransactions[tx.txid]) { - for (const replaced of rbfTransactions[tx.txid]) { + for (const replaced of rbfTransactions[tx.txid].replaced) { replacedTransactions.push({ replaced: replaced.txid, by: tx }); } } @@ -657,10 +718,13 @@ class WebsocketHandler { const addressCache = this.makeAddressCache(newTransactions); const removedAddressCache = this.makeAddressCache(deletedTransactions); + const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations); + this.accelerations = accelerations; + // pre-compute acceleration delta const accelerationUpdate = { - added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null), - removed: accelerationDelta.filter(txid => !accelerations[txid]), + added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null), + removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]), }; // TODO - Fix indentation after PR is merged @@ -947,7 +1011,7 @@ class WebsocketHandler { await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions)); const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap()); - memPool.handleMinedRbfTransactions(rbfTransactions); + memPool.handleRbfTransactions(rbfTransactions); memPool.removeFromSpendMap(transactions); if (config.MEMPOOL.AUDIT && memPool.isInSync()) { @@ -1017,6 +1081,14 @@ class WebsocketHandler { } } + if (config.CORE_RPC.DEBUG_LOG_PATH && block.extras) { + const firstSeen = getRecentFirstSeen(block.id); + if (firstSeen) { + BlocksRepository.$saveFirstSeenTime(block.id, firstSeen); + block.extras.firstSeen = firstSeen; + } + } + const confirmedTxids: { [txid: string]: boolean } = {}; // Update mempool to remove transactions included in the new block @@ -1091,6 +1163,9 @@ class WebsocketHandler { replaced: replacedTransactions, }; + // check for wallet transactions + const walletTransactions = config.WALLETS.ENABLED ? walletApi.processBlock(block, transactions) : []; + const responseCache = { ...this.socketData }; function getCachedResponse(key, data): string { if (!responseCache[key]) { @@ -1295,6 +1370,11 @@ class WebsocketHandler { response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta); } + if (client['track-wallet']) { + const trackedWallet = client['track-wallet']; + response['wallet-transactions'] = getCachedResponse(`wallet-transactions-${trackedWallet}`, walletTransactions[trackedWallet] ?? {}); + } + if (Object.keys(response).length) { client.send(this.serializeResponse(response)); } diff --git a/backend/src/config.ts b/backend/src/config.ts index b0afe7f23..794421551 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -32,6 +32,7 @@ interface IConfig { AUTOMATIC_POOLS_UPDATE: boolean; POOLS_JSON_URL: string, POOLS_JSON_TREE_URL: string, + POOLS_UPDATE_DELAY: number, AUDIT: boolean; RUST_GBT: boolean; LIMIT_GBT: boolean; @@ -85,6 +86,7 @@ interface IConfig { TIMEOUT: number; COOKIE: boolean; COOKIE_PATH: string; + DEBUG_LOG_PATH: string; }; SECOND_CORE_RPC: { HOST: string; @@ -160,6 +162,10 @@ interface IConfig { PAID: boolean; API_KEY: string; }, + WALLETS: { + ENABLED: boolean; + WALLETS: string[]; + } } const defaults: IConfig = { @@ -192,8 +198,9 @@ const defaults: IConfig = { 'AUTOMATIC_POOLS_UPDATE': false, 'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json', 'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', + 'POOLS_UPDATE_DELAY': 604800, // in seconds, default is one week 'AUDIT': false, - 'RUST_GBT': false, + 'RUST_GBT': true, 'LIMIT_GBT': false, 'CPFP_INDEXING': false, 'MAX_BLOCKS_BULK_QUERY': 0, @@ -225,7 +232,8 @@ const defaults: IConfig = { 'PASSWORD': 'mempool', 'TIMEOUT': 60000, 'COOKIE': false, - 'COOKIE_PATH': '/bitcoin/.cookie' + 'COOKIE_PATH': '/bitcoin/.cookie', + 'DEBUG_LOG_PATH': '', }, 'SECOND_CORE_RPC': { 'HOST': '127.0.0.1', @@ -320,6 +328,10 @@ const defaults: IConfig = { 'PAID': false, 'API_KEY': '', }, + 'WALLETS': { + 'ENABLED': false, + 'WALLETS': [], + }, }; class Config implements IConfig { @@ -341,6 +353,7 @@ class Config implements IConfig { MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES']; REDIS: IConfig['REDIS']; FIAT_PRICE: IConfig['FIAT_PRICE']; + WALLETS: IConfig['WALLETS']; constructor() { const configs = this.merge(configFromFile, defaults); @@ -362,6 +375,7 @@ class Config implements IConfig { this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES; this.REDIS = configs.REDIS; this.FIAT_PRICE = configs.FIAT_PRICE; + this.WALLETS = configs.WALLETS; } merge = (...objects: object[]): IConfig => { diff --git a/backend/src/index.ts b/backend/src/index.ts index 1d83c56a3..d939b7423 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -32,6 +32,7 @@ import pricesRoutes from './api/prices/prices.routes'; import miningRoutes from './api/mining/mining-routes'; import liquidRoutes from './api/liquid/liquid.routes'; import bitcoinRoutes from './api/bitcoin/bitcoin.routes'; +import servicesRoutes from './api/services/services-routes'; import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher'; import forensicsService from './tasks/lightning/forensics.service'; import priceUpdater from './tasks/price-updater'; @@ -46,6 +47,7 @@ import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client'; import accelerationRoutes from './api/acceleration/acceleration.routes'; import aboutRoutes from './api/about.routes'; import mempoolBlocks from './api/mempool-blocks'; +import walletApi from './api/services/wallets'; class Server { private wss: WebSocket.Server | undefined; @@ -211,6 +213,8 @@ class Server { } }); } + + poolsUpdater.$startService(); } async runMainUpdateLoop(): Promise { @@ -229,13 +233,17 @@ class Server { const newMempool = await bitcoinApi.$getRawMempool(); const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null; const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1; - const newAccelerations = await accelerationApi.$updateAccelerations(); + const latestAccelerations = await accelerationApi.$updateAccelerations(); const numHandledBlocks = await blocks.$updateBlocks(); const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1); if (numHandledBlocks === 0) { - await memPool.$updateMempool(newMempool, newAccelerations, minFeeMempool, minFeeTip, pollRate); + await memPool.$updateMempool(newMempool, latestAccelerations, minFeeMempool, minFeeTip, pollRate); } indexer.$run(); + if (config.WALLETS.ENABLED) { + // might take a while, so run in the background + walletApi.$syncWallets(); + } if (config.FIAT_PRICE.ENABLED) { priceUpdater.$run(); } @@ -310,8 +318,10 @@ class Server { priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler)); } loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler)); + + accelerationApi.connectWebsocket(); } - + setUpHttpApiRoutes(): void { bitcoinRoutes.initRoutes(this.app); bitcoinCoreRoutes.initRoutes(this.app); @@ -333,6 +343,9 @@ class Server { if (config.MEMPOOL_SERVICES.ACCELERATIONS) { accelerationRoutes.initRoutes(this.app); } + if (config.WALLETS.ENABLED) { + servicesRoutes.initRoutes(this.app); + } if (!config.MEMPOOL.OFFICIAL) { aboutRoutes.initRoutes(this.app); } diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index ccbc94bfa..dc703af21 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -299,6 +299,7 @@ export interface BlockExtension { id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id` name: string; slug: string; + minerNames: string[] | null; }; avgFee: number; avgFeeRate: number; @@ -319,6 +320,7 @@ export interface BlockExtension { segwitTotalSize: number; segwitTotalWeight: number; header: string; + firstSeen: number | null; utxoSetChange: number; // Requires coinstatsindex, will be set to NULL otherwise utxoSetSize: number | null; diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index de6c1deb8..112cb1903 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -14,6 +14,7 @@ import chainTips from '../api/chain-tips'; import blocks from '../api/blocks'; import BlocksAuditsRepository from './BlocksAuditsRepository'; import transactionUtils from '../api/transaction-utils'; +import { parseDATUMTemplateCreator } from '../utils/bitcoin-script'; interface DatabaseBlock { id: string; @@ -56,6 +57,7 @@ interface DatabaseBlock { utxoSetChange: number; utxoSetSize: number; totalInputAmt: number; + firstSeen: number; } const BLOCK_DB_FIELDS = ` @@ -98,7 +100,8 @@ const BLOCK_DB_FIELDS = ` blocks.header, blocks.utxoset_change AS utxoSetChange, blocks.utxoset_size AS utxoSetSize, - blocks.total_input_amt AS totalInputAmt + blocks.total_input_amt AS totalInputAmt, + UNIX_TIMESTAMP(blocks.first_seen) AS firstSeen `; class BlocksRepository { @@ -1020,6 +1023,24 @@ class BlocksRepository { } } + /** + * Save block first seen time + * + * @param id + */ + public async $saveFirstSeenTime(id: string, firstSeen: number): Promise { + try { + await DB.query(` + UPDATE blocks SET first_seen = FROM_UNIXTIME(?) + WHERE hash = ?`, + [firstSeen, id] + ); + } catch (e) { + logger.err(`Cannot update block first seen time. Reason: ` + (e instanceof Error ? e.message : e)); + throw e; + } + } + /** * Convert a mysql row block into a BlockExtended. Note that you * must provide the correct field into dbBlk object param @@ -1054,6 +1075,7 @@ class BlocksRepository { id: dbBlk.poolId, name: dbBlk.poolName, slug: dbBlk.poolSlug, + minerNames: null, }; extras.avgFee = dbBlk.avgFee; extras.avgFeeRate = dbBlk.avgFeeRate; @@ -1076,6 +1098,7 @@ class BlocksRepository { extras.utxoSetSize = dbBlk.utxoSetSize; extras.totalInputAmt = dbBlk.totalInputAmt; extras.virtualSize = dbBlk.weight / 4.0; + extras.firstSeen = dbBlk.firstSeen; // Re-org can happen after indexing so we need to always get the // latest state from core @@ -1123,6 +1146,10 @@ class BlocksRepository { } } + if (extras.pool.name === 'OCEAN') { + extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw); + } + blk.extras = extras; return blk; } diff --git a/backend/src/rpc-api/commands.ts b/backend/src/rpc-api/commands.ts index 85675230b..89ab9cfe6 100644 --- a/backend/src/rpc-api/commands.ts +++ b/backend/src/rpc-api/commands.ts @@ -83,6 +83,7 @@ module.exports = { signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+ stop: 'stop', submitBlock: 'submitblock', // bitcoind v0.7.0+ + submitPackage: 'submitpackage', validateAddress: 'validateaddress', verifyChain: 'verifychain', // bitcoind v0.9.0+ verifyMessage: 'verifymessage', diff --git a/backend/src/tasks/pools-updater.ts b/backend/src/tasks/pools-updater.ts index a3a3265c6..652383a2a 100644 --- a/backend/src/tasks/pools-updater.ts +++ b/backend/src/tasks/pools-updater.ts @@ -6,16 +6,30 @@ import backendInfo from '../api/backend-info'; import logger from '../logger'; import { SocksProxyAgent } from 'socks-proxy-agent'; import * as https from 'https'; +import { Common } from '../api/common'; /** * Maintain the most recent version of pools-v2.json */ class PoolsUpdater { + tag = 'PoolsUpdater'; + lastRun: number = 0; currentSha: string | null = null; poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL; treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL; + public async $startService(): Promise { + while ('Bitcoin is still alive') { + try { + await this.updatePoolsJson(); + } catch (e: any) { + logger.info(`Exception ${e} in PoolsUpdater::$startService. Code: ${e.code}. Message: ${e.message}`, this.tag); + } + await Common.sleep$(10000); + } + } + public async updatePoolsJson(): Promise { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false || config.MEMPOOL.ENABLED === false @@ -23,11 +37,8 @@ class PoolsUpdater { return; } - const oneWeek = 604800; - const oneDay = 86400; - const now = new Date().getTime() / 1000; - if (now - this.lastRun < oneWeek) { // Execute the PoolsUpdate only once a week, or upon restart + if (now - this.lastRun < config.MEMPOOL.POOLS_UPDATE_DELAY) { // Execute the PoolsUpdate only once a week, or upon restart return; } @@ -43,7 +54,7 @@ class PoolsUpdater { this.currentSha = await this.getShaFromDb(); } - logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`); + logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`, this.tag); if (this.currentSha !== null && this.currentSha === githubSha) { return; } @@ -53,16 +64,16 @@ class PoolsUpdater { config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled !process.env.npm_config_update_pools // We're not manually updating mining pool ) { - logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`); - logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`); + logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`, this.tag); + logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`, this.tag); return; } const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet'; if (this.currentSha === null) { - logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining); + logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, this.tag); } else { - logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, logger.tags.mining); + logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, this.tag); } const poolsJson = await this.query(this.poolsUrl); if (poolsJson === undefined) { @@ -71,7 +82,7 @@ class PoolsUpdater { poolsParser.setMiningPools(poolsJson); if (config.DATABASE.ENABLED === false) { // Don't run db operations - logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`); + logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`, this.tag); return; } @@ -81,14 +92,14 @@ class PoolsUpdater { await this.updateDBSha(githubSha); await DB.query('COMMIT;'); } catch (e) { - logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining); + logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, this.tag); await DB.query('ROLLBACK;'); } - logger.info(`Mining pools-v2.json (${githubSha}) import completed`); + logger.info(`Mining pools-v2.json (${githubSha}) import completed`, this.tag); } catch (e) { - this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week - logger.err(`PoolsUpdater failed. Will try again in 24h. Exception: ${JSON.stringify(e)}`, logger.tags.mining); + this.lastRun = now - 600; // Try again in 10 minutes + logger.err(`PoolsUpdater failed. Will try again in 10 minutes. Exception: ${JSON.stringify(e)}`, this.tag); } } @@ -102,7 +113,7 @@ class PoolsUpdater { await DB.query('DELETE FROM state where name="pools_json_sha"'); await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`); } catch (e) { - logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); + logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), this.tag); } } } @@ -115,7 +126,7 @@ class PoolsUpdater { const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"'); return (rows.length > 0 ? rows[0].string : null); } catch (e) { - logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); + logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), this.tag); return null; } } @@ -134,7 +145,7 @@ class PoolsUpdater { } } - logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, logger.tags.mining); + logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, this.tag); return null; } @@ -186,7 +197,7 @@ class PoolsUpdater { } return data.data; } catch (e) { - logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e)); + logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e), this.tag); retry++; } await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL); diff --git a/backend/src/utils/api.ts b/backend/src/utils/api.ts new file mode 100644 index 000000000..69d746b9f --- /dev/null +++ b/backend/src/utils/api.ts @@ -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); + } +} \ No newline at end of file diff --git a/backend/src/utils/bitcoin-script.ts b/backend/src/utils/bitcoin-script.ts index 8f551aa23..f9755fcb4 100644 --- a/backend/src/utils/bitcoin-script.ts +++ b/backend/src/utils/bitcoin-script.ts @@ -200,4 +200,28 @@ export function getVarIntLength(n: number): number { } else { return 9; } +} + +/** Extracts miner names from a DATUM coinbase transaction */ +export function parseDATUMTemplateCreator(coinbaseRaw: string): string[] | null { + let bytes: number[] = []; + for (let c = 0; c < coinbaseRaw.length; c += 2) { + bytes.push(parseInt(coinbaseRaw.slice(c, c + 2), 16)); + } + + // Skip block height + let tagLengthByte = 1 + bytes[0]; + + let tagsLength = bytes[tagLengthByte]; + if (tagsLength == 0x4c) { + tagLengthByte += 1; + tagsLength = bytes[tagLengthByte]; + } + + const tagStart = tagLengthByte + 1; + const tags = bytes.slice(tagStart, tagStart + tagsLength); + let tagString = String.fromCharCode(...tags); + tagString = tagString.replace('\x00', ''); + + return tagString.split('\x0f').map((name) => name.replace(/[^a-zA-Z0-9 ]/g, '')); } \ No newline at end of file diff --git a/backend/src/utils/file-read.ts b/backend/src/utils/file-read.ts new file mode 100644 index 000000000..ddf8660c4 --- /dev/null +++ b/backend/src/utils/file-read.ts @@ -0,0 +1,58 @@ +import * as fs from 'fs'; +import logger from '../logger'; +import config from '../config'; + +function readFile(filePath: string, bufferSize?: number): string[] { + const fileSize = fs.statSync(filePath).size; + const chunkSize = bufferSize || fileSize; + const fileDescriptor = fs.openSync(filePath, 'r'); + const buffer = Buffer.alloc(chunkSize); + + fs.readSync(fileDescriptor, buffer, 0, chunkSize, fileSize - chunkSize); + fs.closeSync(fileDescriptor); + + const lines = buffer.toString('utf8', 0, chunkSize).split('\n'); + return lines; +} + +function extractDateFromLogLine(line: string): number | undefined { + // Extract time from log: "2021-08-31T12:34:56Z" or "2021-08-31T12:34:56.123456Z" + const dateMatch = line.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{6})?Z/); + if (!dateMatch) { + return undefined; + } + + const dateStr = dateMatch[0]; + const date = new Date(dateStr); + let timestamp = Math.floor(date.getTime() / 1000); // Remove decimal (microseconds are added later) + + const timePart = dateStr.split('T')[1]; + const microseconds = timePart.split('.')[1] || ''; + + if (!microseconds) { + return timestamp; + } + + return parseFloat(timestamp + '.' + microseconds); +} + +export function getRecentFirstSeen(hash: string): number | undefined { + const debugLogPath = config.CORE_RPC.DEBUG_LOG_PATH; + if (debugLogPath) { + try { + // Read the last few lines of debug.log + const lines = readFile(debugLogPath, 2048); + + for (let i = lines.length - 1; i >= 0; i--) { + const line = lines[i]; + if (line && line.includes(`Saw new header hash=${hash}`)) { + return extractDateFromLogLine(line); + } + } + } catch (e) { + logger.err(`Cannot parse block first seen time from Core logs. Reason: ` + (e instanceof Error ? e.message : e)); + } + } + + return undefined; +} diff --git a/docker/README.md b/docker/README.md index ce1548e91..2658914eb 100644 --- a/docker/README.md +++ b/docker/README.md @@ -109,6 +109,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over "AUTOMATIC_POOLS_UPDATE": false, "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json", "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", + "POOLS_UPDATE_DELAY": 604800, "CPFP_INDEXING": false, "MAX_BLOCKS_BULK_QUERY": 0, "DISK_CACHE_BLOCK_INTERVAL": 6, @@ -140,6 +141,7 @@ Corresponding `docker-compose.yml` overrides: MEMPOOL_AUTOMATIC_POOLS_UPDATE: "" MEMPOOL_POOLS_JSON_URL: "" MEMPOOL_POOLS_JSON_TREE_URL: "" + MEMPOOL_POOLS_UPDATE_DELAY: "" MEMPOOL_CPFP_INDEXING: "" MEMPOOL_MAX_BLOCKS_BULK_QUERY: "" MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: "" diff --git a/docker/backend/mempool-config.json b/docker/backend/mempool-config.json index 79cd14644..c7ade9b7b 100644 --- a/docker/backend/mempool-config.json +++ b/docker/backend/mempool-config.json @@ -36,6 +36,7 @@ "ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__, "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__", "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__", + "POOLS_UPDATE_DELAY": __MEMPOOL_POOLS_UPDATE_DELAY__, "PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__, "MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__ }, @@ -46,7 +47,8 @@ "PASSWORD": "__CORE_RPC_PASSWORD__", "TIMEOUT": __CORE_RPC_TIMEOUT__, "COOKIE": __CORE_RPC_COOKIE__, - "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__" + "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__", + "DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__" }, "ELECTRUM": { "HOST": "__ELECTRUM_HOST__", diff --git a/docker/backend/start.sh b/docker/backend/start.sh index 8033531ef..d4765972e 100755 --- a/docker/backend/start.sh +++ b/docker/backend/start.sh @@ -29,8 +29,9 @@ __MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info} __MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=false} __MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json} __MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master} +__MEMPOOL_POOLS_UPDATE_DELAY__=${MEMPOOL_POOLS_UPDATE_DELAY:=604800} __MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false} -__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=false} +__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=true} __MEMPOOL_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false} __MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false} __MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0} @@ -48,6 +49,7 @@ __CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool} __CORE_RPC_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000} __CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false} __CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""} +__CORE_RPC_DEBUG_LOG_PATH__=${CORE_RPC_DEBUG_LOG_PATH:=""} # ELECTRUM __ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1} @@ -187,6 +189,7 @@ sed -i "s!__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__!${__MEMPOOL_STDOUT_LOG_MIN_PRIORIT sed -i "s!__MEMPOOL_AUTOMATIC_POOLS_UPDATE__!${__MEMPOOL_AUTOMATIC_POOLS_UPDATE__}!g" mempool-config.json sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json +sed -i "s!__MEMPOOL_POOLS_UPDATE_DELAY__!${__MEMPOOL_POOLS_UPDATE_DELAY__}!g" mempool-config.json sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!g" mempool-config.json sed -i "s!__MEMPOOL_LIMIT_GBT__!${__MEMPOOL_LIMIT_GBT__}!g" mempool-config.json @@ -205,6 +208,7 @@ sed -i "s!__CORE_RPC_PASSWORD__!${__CORE_RPC_PASSWORD__}!g" mempool-config.json sed -i "s!__CORE_RPC_TIMEOUT__!${__CORE_RPC_TIMEOUT__}!g" mempool-config.json sed -i "s!__CORE_RPC_COOKIE__!${__CORE_RPC_COOKIE__}!g" mempool-config.json sed -i "s!__CORE_RPC_COOKIE_PATH__!${__CORE_RPC_COOKIE_PATH__}!g" mempool-config.json +sed -i "s!__CORE_RPC_DEBUG_LOG_PATH__!${__CORE_RPC_DEBUG_LOG_PATH__}!g" mempool-config.json sed -i "s!__ELECTRUM_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!g" mempool-config.json diff --git a/frontend/custom-bitb-config.json b/frontend/custom-bitb-config.json new file mode 100644 index 000000000..4938034fe --- /dev/null +++ b/frontend/custom-bitb-config.json @@ -0,0 +1,48 @@ +{ + "theme": "wiz", + "enterprise": "bitb", + "branding": { + "name": "bitb", + "title": "BITB", + "site_id": 20, + "header_img": "/resources/bitblogo.svg", + "footer_img": "/resources/bitblogo.svg" + }, + "dashboard": { + "widgets": [ + { + "component": "fees", + "mobileOrder": 4 + }, + { + "component": "walletBalance", + "mobileOrder": 1, + "props": { + "wallet": "BITB" + } + }, + { + "component": "goggles", + "mobileOrder": 5 + }, + { + "component": "wallet", + "mobileOrder": 2, + "props": { + "wallet": "BITB", + "period": "all" + } + }, + { + "component": "blocks" + }, + { + "component": "walletTransactions", + "mobileOrder": 3, + "props": { + "wallet": "BITB" + } + } + ] + } +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c17e706af..a27bffcb4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -32,10 +32,9 @@ "bootstrap": "~4.6.2", "browserify": "^17.0.0", "clipboard": "^2.0.11", - "cypress": "^13.14.0", "domino": "^2.1.6", "echarts": "~5.5.0", - "esbuild": "^0.23.0", + "esbuild": "^0.24.0", "lightweight-charts": "~3.8.0", "ngx-echarts": "~17.2.0", "ngx-infinite-scroll": "^17.0.0", @@ -43,7 +42,7 @@ "rxjs": "~7.8.1", "tinyify": "^4.0.0", "tlite": "^0.1.9", - "tslib": "~2.7.0", + "tslib": "~2.8.0", "zone.js": "~0.14.4" }, "devDependencies": { @@ -52,7 +51,7 @@ "@types/node": "^18.11.9", "@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/parser": "^7.4.0", - "browser-sync": "^3.0.0", + "browser-sync": "^3.0.3", "eslint": "^8.57.0", "http-proxy-middleware": "~2.0.6", "prettier": "^3.0.0", @@ -63,7 +62,7 @@ "optionalDependencies": { "@cypress/schematic": "^2.5.0", "@types/cypress": "^1.1.3", - "cypress": "^13.14.0", + "cypress": "^13.15.0", "cypress-fail-on-console-error": "~5.1.0", "cypress-wait-until": "^2.0.1", "mock-socket": "~9.3.1", @@ -3114,9 +3113,9 @@ } }, "node_modules/@cypress/request": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", - "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz", + "integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==", "optional": true, "dependencies": { "aws-sign2": "~0.7.0", @@ -3125,14 +3124,14 @@ "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "http-signature": "~1.3.6", + "form-data": "~4.0.0", + "http-signature": "~1.4.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "6.10.4", + "qs": "6.13.0", "safe-buffer": "^5.1.2", "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", @@ -3202,9 +3201,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", - "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", "cpu": [ "ppc64" ], @@ -3217,9 +3216,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", - "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", "cpu": [ "arm" ], @@ -3232,9 +3231,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", - "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", "cpu": [ "arm64" ], @@ -3247,9 +3246,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", - "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", "cpu": [ "x64" ], @@ -3262,9 +3261,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", - "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", "cpu": [ "arm64" ], @@ -3277,9 +3276,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", - "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", "cpu": [ "x64" ], @@ -3292,9 +3291,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", - "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", "cpu": [ "arm64" ], @@ -3307,9 +3306,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", - "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", "cpu": [ "x64" ], @@ -3322,9 +3321,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", - "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", "cpu": [ "arm" ], @@ -3337,9 +3336,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", - "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", "cpu": [ "arm64" ], @@ -3352,9 +3351,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", - "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", "cpu": [ "ia32" ], @@ -3367,9 +3366,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", - "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", "cpu": [ "loong64" ], @@ -3382,9 +3381,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", - "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", "cpu": [ "mips64el" ], @@ -3397,9 +3396,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", - "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", "cpu": [ "ppc64" ], @@ -3412,9 +3411,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", - "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", "cpu": [ "riscv64" ], @@ -3427,9 +3426,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", - "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", "cpu": [ "s390x" ], @@ -3442,9 +3441,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", - "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", "cpu": [ "x64" ], @@ -3457,9 +3456,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", - "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", "cpu": [ "x64" ], @@ -3472,9 +3471,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", - "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", "cpu": [ "arm64" ], @@ -3487,9 +3486,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", - "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", "cpu": [ "x64" ], @@ -3502,9 +3501,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", - "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", "cpu": [ "x64" ], @@ -3517,9 +3516,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", - "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", "cpu": [ "arm64" ], @@ -3532,9 +3531,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", - "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", "cpu": [ "ia32" ], @@ -3547,9 +3546,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", - "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", "cpu": [ "x64" ], @@ -4314,9 +4313,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", - "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", "cpu": [ "arm" ], @@ -4326,9 +4325,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", - "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", "cpu": [ "arm64" ], @@ -4338,9 +4337,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", - "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", "cpu": [ "arm64" ], @@ -4350,9 +4349,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", - "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", "cpu": [ "x64" ], @@ -4362,9 +4361,21 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", - "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", "cpu": [ "arm" ], @@ -4374,9 +4385,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", - "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", "cpu": [ "arm64" ], @@ -4386,9 +4397,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", - "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", "cpu": [ "arm64" ], @@ -4397,10 +4408,22 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", - "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", "cpu": [ "riscv64" ], @@ -4409,10 +4432,22 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", - "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", "cpu": [ "x64" ], @@ -4422,9 +4457,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", - "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", "cpu": [ "x64" ], @@ -4434,9 +4469,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", - "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", "cpu": [ "arm64" ], @@ -4446,9 +4481,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", - "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", "cpu": [ "ia32" ], @@ -4458,9 +4493,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", - "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", "cpu": [ "x64" ], @@ -4765,9 +4800,9 @@ "devOptional": true }, "node_modules/@types/cors": { - "version": "2.8.13", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", - "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "devOptional": true, "dependencies": { "@types/node": "*" @@ -4802,9 +4837,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, "node_modules/@types/express": { "version": "4.17.13", @@ -5798,9 +5833,9 @@ } }, "node_modules/aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", "optional": true }, "node_modules/axios": { @@ -6020,9 +6055,9 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -6032,7 +6067,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -6066,20 +6101,6 @@ "node": ">= 0.8" } }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/bonjour-service": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", @@ -6188,13 +6209,13 @@ } }, "node_modules/browser-sync": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.2.tgz", - "integrity": "sha512-PC9c7aWJFVR4IFySrJxOqLwB9ENn3/TaXCXtAa0SzLwocLN3qMjN+IatbjvtCX92BjNXsY6YWg9Eb7F3Wy255g==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.3.tgz", + "integrity": "sha512-91hoBHKk1C4pGeD+oE9Ld222k2GNQEAsI5AElqR8iLLWNrmZR2LPP8B0h8dpld9u7kro5IEUB3pUb0DJ3n1cRQ==", "devOptional": true, "dependencies": { - "browser-sync-client": "^3.0.2", - "browser-sync-ui": "^3.0.2", + "browser-sync-client": "^3.0.3", + "browser-sync-ui": "^3.0.3", "bs-recipes": "1.3.4", "chalk": "4.1.2", "chokidar": "^3.5.1", @@ -6208,15 +6229,15 @@ "fs-extra": "3.0.1", "http-proxy": "^1.18.1", "immutable": "^3", - "micromatch": "^4.0.2", + "micromatch": "^4.0.8", "opn": "5.3.0", "portscanner": "2.2.0", "raw-body": "^2.3.2", "resp-modifier": "6.0.2", "rx": "4.1.0", - "send": "0.16.2", - "serve-index": "1.9.1", - "serve-static": "1.13.2", + "send": "^0.19.0", + "serve-index": "^1.9.1", + "serve-static": "^1.16.2", "server-destroy": "1.0.1", "socket.io": "^4.4.1", "ua-parser-js": "^1.0.33", @@ -6230,9 +6251,9 @@ } }, "node_modules/browser-sync-client": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.2.tgz", - "integrity": "sha512-tBWdfn9L0wd2Pjuz/NWHtNEKthVb1Y67vg8/qyGNtCqetNz5lkDkFnrsx5UhPNPYUO8vci50IWC/BhYaQskDiQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.3.tgz", + "integrity": "sha512-TOEXaMgYNjBYIcmX5zDlOdjEqCeCN/d7opf/fuyUD/hhGVCfP54iQIDhENCi012AqzYZm3BvuFl57vbwSTwkSQ==", "devOptional": true, "dependencies": { "etag": "1.8.1", @@ -6244,9 +6265,9 @@ } }, "node_modules/browser-sync-ui": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.2.tgz", - "integrity": "sha512-V3FwWAI+abVbFLTyJjXJlCMBwjc3GXf/BPGfwO2fMFACWbIGW9/4SrBOFYEOOtqzCjQE0Di+U3VIb7eES4omNA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.3.tgz", + "integrity": "sha512-FcGWo5lP5VodPY6O/f4pXQy5FFh4JK0f2/fTBsp0Lx1NtyBWs/IfPPJbW8m1ujTW/2r07oUXKTF2LYZlCZktjw==", "devOptional": true, "dependencies": { "async-each-series": "0.1.1", @@ -6391,30 +6412,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "devOptional": true }, - "node_modules/browser-sync/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "devOptional": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/browser-sync/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "devOptional": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/browser-sync/node_modules/destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==", - "devOptional": true - }, "node_modules/browser-sync/node_modules/fs-extra": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", @@ -6435,27 +6432,6 @@ "node": ">=8" } }, - "node_modules/browser-sync/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "devOptional": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/browser-sync/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "devOptional": true - }, "node_modules/browser-sync/node_modules/jsonfile": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", @@ -6465,75 +6441,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/browser-sync/node_modules/mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", - "devOptional": true, - "bin": { - "mime": "cli.js" - } - }, - "node_modules/browser-sync/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "devOptional": true - }, - "node_modules/browser-sync/node_modules/send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "devOptional": true, - "dependencies": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/browser-sync/node_modules/serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "devOptional": true, - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/browser-sync/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "devOptional": true - }, - "node_modules/browser-sync/node_modules/statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", - "devOptional": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/browser-sync/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -7674,9 +7581,9 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "engines": { "node": ">= 0.6" } @@ -8046,13 +7953,13 @@ "peer": true }, "node_modules/cypress": { - "version": "13.14.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.0.tgz", - "integrity": "sha512-r0+nhd033x883YL6068futewUsl02Q7rWiinyAAIBDW/OOTn+UMILWgNuCiY3vtJjd53efOqq5R9dctQk/rKiw==", + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.0.tgz", + "integrity": "sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==", "hasInstallScript": true, "optional": true, "dependencies": { - "@cypress/request": "^3.0.1", + "@cypress/request": "^3.0.4", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", @@ -8885,9 +8792,9 @@ } }, "node_modules/engine.io": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.1.tgz", - "integrity": "sha512-mGqhI+D7YxS9KJMppR6Iuo37Ed3abhU8NdfgSvJSDUafQutrN+sPTncJYTyM9+tkhSmWodKtVYGPPHyXJEwEQA==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", "devOptional": true, "dependencies": { "@types/cookie": "^0.4.1", @@ -8895,98 +8802,47 @@ "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "~0.4.1", + "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.1.0", - "ws": "~8.11.0" + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz", + "integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==", "devOptional": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", - "xmlhttprequest-ssl": "~2.0.0" - } - }, - "node_modules/engine.io-client/node_modules/engine.io-parser": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", - "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", - "devOptional": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" } }, "node_modules/engine.io-parser": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz", - "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "devOptional": true, "engines": { "node": ">=10.0.0" } }, "node_modules/engine.io/node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "devOptional": true, "engines": { "node": ">= 0.6" } }, - "node_modules/engine.io/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -9211,9 +9067,9 @@ } }, "node_modules/esbuild": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", - "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -9222,30 +9078,30 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.0", - "@esbuild/android-arm": "0.23.0", - "@esbuild/android-arm64": "0.23.0", - "@esbuild/android-x64": "0.23.0", - "@esbuild/darwin-arm64": "0.23.0", - "@esbuild/darwin-x64": "0.23.0", - "@esbuild/freebsd-arm64": "0.23.0", - "@esbuild/freebsd-x64": "0.23.0", - "@esbuild/linux-arm": "0.23.0", - "@esbuild/linux-arm64": "0.23.0", - "@esbuild/linux-ia32": "0.23.0", - "@esbuild/linux-loong64": "0.23.0", - "@esbuild/linux-mips64el": "0.23.0", - "@esbuild/linux-ppc64": "0.23.0", - "@esbuild/linux-riscv64": "0.23.0", - "@esbuild/linux-s390x": "0.23.0", - "@esbuild/linux-x64": "0.23.0", - "@esbuild/netbsd-x64": "0.23.0", - "@esbuild/openbsd-arm64": "0.23.0", - "@esbuild/openbsd-x64": "0.23.0", - "@esbuild/sunos-x64": "0.23.0", - "@esbuild/win32-arm64": "0.23.0", - "@esbuild/win32-ia32": "0.23.0", - "@esbuild/win32-x64": "0.23.0" + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" } }, "node_modules/esbuild-wasm": { @@ -9876,36 +9732,36 @@ "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -9924,6 +9780,14 @@ "ms": "2.0.0" } }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/express/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -9940,20 +9804,6 @@ "node": ">= 0.8" } }, - "node_modules/express/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/express/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -10178,12 +10028,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -10202,6 +10052,14 @@ "ms": "2.0.0" } }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -10341,17 +10199,17 @@ } }, "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "optional": true, "dependencies": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", "mime-types": "^2.1.12" }, "engines": { - "node": ">= 0.12" + "node": ">= 6" } }, "node_modules/forwarded": { @@ -10993,14 +10851,14 @@ } }, "node_modules/http-signature": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", - "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", "optional": true, "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^2.0.2", - "sshpk": "^1.14.1" + "sshpk": "^1.18.0" }, "engines": { "node": ">=0.10" @@ -12668,9 +12526,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -12694,12 +12555,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" @@ -13388,9 +13249,9 @@ "optional": true }, "node_modules/nise/node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "optional": true, "dependencies": { "isarray": "0.0.1" @@ -13675,9 +13536,12 @@ } }, "node_modules/object-inspect": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", - "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -14191,9 +14055,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/path-type": { "version": "4.0.0", @@ -14767,12 +14631,11 @@ } }, "node_modules/qs": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", - "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", - "optional": true, + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -15228,11 +15091,11 @@ } }, "node_modules/rollup": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", - "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -15242,19 +15105,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.13.0", - "@rollup/rollup-android-arm64": "4.13.0", - "@rollup/rollup-darwin-arm64": "4.13.0", - "@rollup/rollup-darwin-x64": "4.13.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", - "@rollup/rollup-linux-arm64-gnu": "4.13.0", - "@rollup/rollup-linux-arm64-musl": "4.13.0", - "@rollup/rollup-linux-riscv64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-musl": "4.13.0", - "@rollup/rollup-win32-arm64-msvc": "4.13.0", - "@rollup/rollup-win32-ia32-msvc": "4.13.0", - "@rollup/rollup-win32-x64-msvc": "4.13.0", + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", "fsevents": "~2.3.2" } }, @@ -15478,9 +15344,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -15619,19 +15485,27 @@ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/server-destroy": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", @@ -15723,13 +15597,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -15901,62 +15779,42 @@ } }, "node_modules/socket.io": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.1.tgz", - "integrity": "sha512-W+utHys2w//dhFjy7iQQu9sGd3eokCjGbl2r59tyLqNiJJBdIebn3GAKEXBr3osqHTObJi2die/25bCx2zsaaw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", "devOptional": true, "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.5.0", + "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/socket.io-adapter": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", - "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", "devOptional": true, "dependencies": { - "ws": "~8.11.0" - } - }, - "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "debug": "~4.3.4", + "ws": "~8.17.1" } }, "node_modules/socket.io-client": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", - "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz", + "integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==", "devOptional": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", - "engine.io-client": "~6.5.2", + "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" }, "engines": { @@ -16167,9 +16025,9 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" }, "node_modules/sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "optional": true, "dependencies": { "asn1": "~0.2.3", @@ -16763,9 +16621,9 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "optional": true, "dependencies": { "psl": "^1.1.33", @@ -16931,9 +16789,9 @@ } }, "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" }, "node_modules/tuf-js": { "version": "2.2.0", @@ -17827,30 +17685,16 @@ } }, "node_modules/wait-on/node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "optional": true, "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, - "node_modules/wait-on/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "optional": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/wait-on/node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -18304,9 +18148,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "engines": { "node": ">=10.0.0" }, @@ -18332,9 +18176,9 @@ } }, "node_modules/xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz", + "integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==", "devOptional": true, "engines": { "node": ">=0.4.0" @@ -20504,9 +20348,9 @@ } }, "@cypress/request": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", - "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz", + "integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==", "optional": true, "requires": { "aws-sign2": "~0.7.0", @@ -20515,14 +20359,14 @@ "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "http-signature": "~1.3.6", + "form-data": "~4.0.0", + "http-signature": "~1.4.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "6.10.4", + "qs": "6.13.0", "safe-buffer": "^5.1.2", "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", @@ -20583,147 +20427,147 @@ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==" }, "@esbuild/aix-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", - "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", "optional": true }, "@esbuild/android-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", - "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", "optional": true }, "@esbuild/android-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", - "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", "optional": true }, "@esbuild/android-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", - "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", - "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", "optional": true }, "@esbuild/darwin-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", - "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", - "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", - "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", "optional": true }, "@esbuild/linux-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", - "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", "optional": true }, "@esbuild/linux-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", - "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", "optional": true }, "@esbuild/linux-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", - "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", "optional": true }, "@esbuild/linux-loong64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", - "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", - "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", - "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", - "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", "optional": true }, "@esbuild/linux-s390x": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", - "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", "optional": true }, "@esbuild/linux-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", - "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", - "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", "optional": true }, "@esbuild/openbsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", - "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", - "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", "optional": true }, "@esbuild/sunos-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", - "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", "optional": true }, "@esbuild/win32-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", - "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", "optional": true }, "@esbuild/win32-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", - "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", "optional": true }, "@esbuild/win32-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", - "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", "optional": true }, "@eslint-community/eslint-utils": { @@ -21267,81 +21111,99 @@ "peer": true }, "@rollup/rollup-android-arm-eabi": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", - "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", "optional": true }, "@rollup/rollup-android-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", - "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", "optional": true }, "@rollup/rollup-darwin-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", - "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", "optional": true }, "@rollup/rollup-darwin-x64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", - "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", "optional": true }, "@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", - "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "optional": true + }, + "@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", "optional": true }, "@rollup/rollup-linux-arm64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", - "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", "optional": true }, "@rollup/rollup-linux-arm64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", - "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "optional": true + }, + "@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", "optional": true }, "@rollup/rollup-linux-riscv64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", - "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "optional": true + }, + "@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", "optional": true }, "@rollup/rollup-linux-x64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", - "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", "optional": true }, "@rollup/rollup-linux-x64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", - "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", "optional": true }, "@rollup/rollup-win32-arm64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", - "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", "optional": true }, "@rollup/rollup-win32-ia32-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", - "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", "optional": true }, "@rollup/rollup-win32-x64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", - "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", "optional": true }, "@schematics/angular": { @@ -21609,9 +21471,9 @@ "devOptional": true }, "@types/cors": { - "version": "2.8.13", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", - "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "devOptional": true, "requires": { "@types/node": "*" @@ -21645,9 +21507,9 @@ } }, "@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, "@types/express": { "version": "4.17.13", @@ -22407,9 +22269,9 @@ "optional": true }, "aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", "optional": true }, "axios": { @@ -22583,9 +22445,9 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "requires": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -22595,7 +22457,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -22621,14 +22483,6 @@ "requires": { "ee-first": "1.1.1" } - }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "requires": { - "side-channel": "^1.0.4" - } } } }, @@ -22718,13 +22572,13 @@ } }, "browser-sync": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.2.tgz", - "integrity": "sha512-PC9c7aWJFVR4IFySrJxOqLwB9ENn3/TaXCXtAa0SzLwocLN3qMjN+IatbjvtCX92BjNXsY6YWg9Eb7F3Wy255g==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.3.tgz", + "integrity": "sha512-91hoBHKk1C4pGeD+oE9Ld222k2GNQEAsI5AElqR8iLLWNrmZR2LPP8B0h8dpld9u7kro5IEUB3pUb0DJ3n1cRQ==", "devOptional": true, "requires": { - "browser-sync-client": "^3.0.2", - "browser-sync-ui": "^3.0.2", + "browser-sync-client": "^3.0.3", + "browser-sync-ui": "^3.0.3", "bs-recipes": "1.3.4", "chalk": "4.1.2", "chokidar": "^3.5.1", @@ -22738,15 +22592,15 @@ "fs-extra": "3.0.1", "http-proxy": "^1.18.1", "immutable": "^3", - "micromatch": "^4.0.2", + "micromatch": "^4.0.8", "opn": "5.3.0", "portscanner": "2.2.0", "raw-body": "^2.3.2", "resp-modifier": "6.0.2", "rx": "4.1.0", - "send": "0.16.2", - "serve-index": "1.9.1", - "serve-static": "1.13.2", + "send": "^0.19.0", + "serve-index": "^1.9.1", + "serve-static": "^1.16.2", "server-destroy": "1.0.1", "socket.io": "^4.4.1", "ua-parser-js": "^1.0.33", @@ -22798,27 +22652,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "devOptional": true }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "devOptional": true, - "requires": { - "ms": "2.0.0" - } - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "devOptional": true - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==", - "devOptional": true - }, "fs-extra": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", @@ -22836,24 +22669,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "devOptional": true }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "devOptional": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "devOptional": true - }, "jsonfile": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", @@ -22863,63 +22678,6 @@ "graceful-fs": "^4.1.6" } }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", - "devOptional": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "devOptional": true - }, - "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "devOptional": true, - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" - } - }, - "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "devOptional": true, - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "devOptional": true - }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", - "devOptional": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -22982,9 +22740,9 @@ } }, "browser-sync-client": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.2.tgz", - "integrity": "sha512-tBWdfn9L0wd2Pjuz/NWHtNEKthVb1Y67vg8/qyGNtCqetNz5lkDkFnrsx5UhPNPYUO8vci50IWC/BhYaQskDiQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.3.tgz", + "integrity": "sha512-TOEXaMgYNjBYIcmX5zDlOdjEqCeCN/d7opf/fuyUD/hhGVCfP54iQIDhENCi012AqzYZm3BvuFl57vbwSTwkSQ==", "devOptional": true, "requires": { "etag": "1.8.1", @@ -22993,9 +22751,9 @@ } }, "browser-sync-ui": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.2.tgz", - "integrity": "sha512-V3FwWAI+abVbFLTyJjXJlCMBwjc3GXf/BPGfwO2fMFACWbIGW9/4SrBOFYEOOtqzCjQE0Di+U3VIb7eES4omNA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.3.tgz", + "integrity": "sha512-FcGWo5lP5VodPY6O/f4pXQy5FFh4JK0f2/fTBsp0Lx1NtyBWs/IfPPJbW8m1ujTW/2r07oUXKTF2LYZlCZktjw==", "devOptional": true, "requires": { "async-each-series": "0.1.1", @@ -23865,9 +23623,9 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, "cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" }, "cookie-signature": { "version": "1.0.6", @@ -24138,12 +23896,12 @@ "peer": true }, "cypress": { - "version": "13.14.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.0.tgz", - "integrity": "sha512-r0+nhd033x883YL6068futewUsl02Q7rWiinyAAIBDW/OOTn+UMILWgNuCiY3vtJjd53efOqq5R9dctQk/rKiw==", + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.0.tgz", + "integrity": "sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==", "optional": true, "requires": { - "@cypress/request": "^3.0.1", + "@cypress/request": "^3.0.4", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", @@ -24803,9 +24561,9 @@ } }, "engine.io": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.1.tgz", - "integrity": "sha512-mGqhI+D7YxS9KJMppR6Iuo37Ed3abhU8NdfgSvJSDUafQutrN+sPTncJYTyM9+tkhSmWodKtVYGPPHyXJEwEQA==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", "devOptional": true, "requires": { "@types/cookie": "^0.4.1", @@ -24813,60 +24571,38 @@ "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "~0.4.1", + "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.1.0", - "ws": "~8.11.0" + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" }, "dependencies": { "cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "devOptional": true - }, - "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "requires": {} } } }, "engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz", + "integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==", "devOptional": true, "requires": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", - "xmlhttprequest-ssl": "~2.0.0" - }, - "dependencies": { - "engine.io-parser": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", - "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", - "devOptional": true - }, - "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "requires": {} - } + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" } }, "engine.io-parser": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz", - "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "devOptional": true }, "enhanced-resolve": { @@ -25055,34 +24791,34 @@ } }, "esbuild": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", - "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", "requires": { - "@esbuild/aix-ppc64": "0.23.0", - "@esbuild/android-arm": "0.23.0", - "@esbuild/android-arm64": "0.23.0", - "@esbuild/android-x64": "0.23.0", - "@esbuild/darwin-arm64": "0.23.0", - "@esbuild/darwin-x64": "0.23.0", - "@esbuild/freebsd-arm64": "0.23.0", - "@esbuild/freebsd-x64": "0.23.0", - "@esbuild/linux-arm": "0.23.0", - "@esbuild/linux-arm64": "0.23.0", - "@esbuild/linux-ia32": "0.23.0", - "@esbuild/linux-loong64": "0.23.0", - "@esbuild/linux-mips64el": "0.23.0", - "@esbuild/linux-ppc64": "0.23.0", - "@esbuild/linux-riscv64": "0.23.0", - "@esbuild/linux-s390x": "0.23.0", - "@esbuild/linux-x64": "0.23.0", - "@esbuild/netbsd-x64": "0.23.0", - "@esbuild/openbsd-arm64": "0.23.0", - "@esbuild/openbsd-x64": "0.23.0", - "@esbuild/sunos-x64": "0.23.0", - "@esbuild/win32-arm64": "0.23.0", - "@esbuild/win32-ia32": "0.23.0", - "@esbuild/win32-x64": "0.23.0" + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" } }, "esbuild-wasm": { @@ -25551,36 +25287,36 @@ "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" }, "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -25596,6 +25332,11 @@ "ms": "2.0.0" } }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -25609,14 +25350,6 @@ "ee-first": "1.1.1" } }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "requires": { - "side-channel": "^1.0.4" - } - }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -25789,12 +25522,12 @@ } }, "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "requires": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -25810,6 +25543,11 @@ "ms": "2.0.0" } }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -25903,13 +25641,13 @@ "optional": true }, "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "optional": true, "requires": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, @@ -26371,14 +26109,14 @@ } }, "http-signature": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", - "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", "optional": true, "requires": { "assert-plus": "^1.0.0", "jsprim": "^2.0.2", - "sshpk": "^1.14.1" + "sshpk": "^1.18.0" } }, "https-browserify": { @@ -27602,9 +27340,9 @@ } }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" }, "merge-stream": { "version": "2.0.0", @@ -27622,12 +27360,12 @@ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "braces": "^3.0.3", + "picomatch": "^2.3.1" } }, "miller-rabin": { @@ -28167,9 +27905,9 @@ "optional": true }, "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "optional": true, "requires": { "isarray": "0.0.1" @@ -28375,9 +28113,9 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-inspect": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", - "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" }, "object-keys": { "version": "1.1.1", @@ -28751,9 +28489,9 @@ } }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "path-type": { "version": "4.0.0", @@ -29148,12 +28886,11 @@ } }, "qs": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", - "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", - "optional": true, + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "querystring": { @@ -29506,24 +29243,27 @@ } }, "rollup": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", - "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", "requires": { - "@rollup/rollup-android-arm-eabi": "4.13.0", - "@rollup/rollup-android-arm64": "4.13.0", - "@rollup/rollup-darwin-arm64": "4.13.0", - "@rollup/rollup-darwin-x64": "4.13.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", - "@rollup/rollup-linux-arm64-gnu": "4.13.0", - "@rollup/rollup-linux-arm64-musl": "4.13.0", - "@rollup/rollup-linux-riscv64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-musl": "4.13.0", - "@rollup/rollup-win32-arm64-msvc": "4.13.0", - "@rollup/rollup-win32-ia32-msvc": "4.13.0", - "@rollup/rollup-win32-x64-msvc": "4.13.0", - "@types/estree": "1.0.5", + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", + "@types/estree": "1.0.6", "fsevents": "~2.3.2" } }, @@ -29674,9 +29414,9 @@ } }, "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "requires": { "debug": "2.6.9", "depd": "2.0.0", @@ -29797,14 +29537,21 @@ } }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "requires": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + } } }, "server-destroy": { @@ -29880,13 +29627,14 @@ "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==" }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "signal-exit": { @@ -30004,47 +29752,39 @@ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" }, "socket.io": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.1.tgz", - "integrity": "sha512-W+utHys2w//dhFjy7iQQu9sGd3eokCjGbl2r59tyLqNiJJBdIebn3GAKEXBr3osqHTObJi2die/25bCx2zsaaw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", "devOptional": true, "requires": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.5.0", + "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "socket.io-adapter": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", - "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", "devOptional": true, "requires": { - "ws": "~8.11.0" - }, - "dependencies": { - "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "requires": {} - } + "debug": "~4.3.4", + "ws": "~8.17.1" } }, "socket.io-client": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", - "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz", + "integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==", "devOptional": true, "requires": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", - "engine.io-client": "~6.5.2", + "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, @@ -30217,9 +29957,9 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" }, "sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "optional": true, "requires": { "asn1": "~0.2.3", @@ -30665,9 +30405,9 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, "tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "optional": true, "requires": { "psl": "^1.1.33", @@ -30774,9 +30514,9 @@ } }, "tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" }, "tuf-js": { "version": "2.2.0", @@ -31288,27 +31028,16 @@ }, "dependencies": { "axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "optional": true, "requires": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "optional": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, "proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -31623,9 +31352,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "requires": {} }, "xhr2": { @@ -31634,9 +31363,9 @@ "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==" }, "xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz", + "integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==", "devOptional": true }, "xtend": { diff --git a/frontend/package.json b/frontend/package.json index 3b5d61be0..6a0d7dc12 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -92,10 +92,10 @@ "ngx-infinite-scroll": "^17.0.0", "qrcode": "1.5.1", "rxjs": "~7.8.1", - "esbuild": "^0.23.0", + "esbuild": "^0.24.0", "tinyify": "^4.0.0", "tlite": "^0.1.9", - "tslib": "~2.7.0", + "tslib": "~2.8.0", "zone.js": "~0.14.4" }, "devDependencies": { @@ -105,7 +105,7 @@ "@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/parser": "^7.4.0", "eslint": "^8.57.0", - "browser-sync": "^3.0.0", + "browser-sync": "^3.0.3", "http-proxy-middleware": "~2.0.6", "prettier": "^3.0.0", "source-map-support": "^0.5.21", @@ -115,7 +115,7 @@ "optionalDependencies": { "@cypress/schematic": "^2.5.0", "@types/cypress": "^1.1.3", - "cypress": "^13.14.0", + "cypress": "^13.15.0", "cypress-fail-on-console-error": "~5.1.0", "cypress-wait-until": "^2.0.1", "mock-socket": "~9.3.1", diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 1f2e3f531..d1748312d 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -1,15 +1,15 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; -import { AppPreloadingStrategy } from './app.preloading-strategy' -import { BlockViewComponent } from './components/block-view/block-view.component'; -import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component'; -import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component'; -import { ClockComponent } from './components/clock/clock.component'; -import { StatusViewComponent } from './components/status-view/status-view.component'; -import { AddressGroupComponent } from './components/address-group/address-group.component'; -import { TrackerComponent } from './components/tracker/tracker.component'; -import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component'; -import { TrackerGuard } from './route-guards'; +import { AppPreloadingStrategy } from '@app/app.preloading-strategy' +import { BlockViewComponent } from '@components/block-view/block-view.component'; +import { EightBlocksComponent } from '@components/eight-blocks/eight-blocks.component'; +import { MempoolBlockViewComponent } from '@components/mempool-block-view/mempool-block-view.component'; +import { ClockComponent } from '@components/clock/clock.component'; +import { StatusViewComponent } from '@components/status-view/status-view.component'; +import { AddressGroupComponent } from '@components/address-group/address-group.component'; +import { TrackerComponent } from '@components/tracker/tracker.component'; +import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component'; +import { TrackerGuard } from '@app/route-guards'; const browserWindow = window || {}; // @ts-ignore @@ -22,16 +22,16 @@ let routes: Routes = [ { path: '', pathMatch: 'full', - loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), data: { preload: true }, }, { path: '', - loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), + loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule), data: { preload: true }, }, { - path: 'wallet', + path: 'widget/wallet', children: [], component: AddressGroupComponent, data: { @@ -45,7 +45,7 @@ let routes: Routes = [ }, { path: '', - loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), data: { preload: true }, }, { @@ -60,12 +60,12 @@ let routes: Routes = [ { path: '', pathMatch: 'full', - loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), data: { preload: true }, }, { path: '', - loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), + loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule), data: { preload: true }, }, { @@ -83,7 +83,7 @@ let routes: Routes = [ }, { path: '', - loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), data: { preload: true }, }, { @@ -103,16 +103,16 @@ let routes: Routes = [ { path: '', pathMatch: 'full', - loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), data: { preload: true }, }, { path: '', - loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), + loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule), data: { preload: true }, }, { - path: 'wallet', + path: 'widget/wallet', children: [], component: AddressGroupComponent, data: { @@ -126,7 +126,7 @@ let routes: Routes = [ }, { path: '', - loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), data: { preload: true }, }, { @@ -138,22 +138,22 @@ let routes: Routes = [ { path: '', pathMatch: 'full', - loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), data: { preload: true }, }, { path: 'tx', canMatch: [TrackerGuard], runGuardsAndResolvers: 'always', - loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule), + loadChildren: () => import('@components/tracker/tracker.module').then(m => m.TrackerModule), }, { path: '', - loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), + loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule), data: { preload: true }, }, { - path: 'wallet', + path: 'widget/wallet', children: [], component: AddressGroupComponent, data: { @@ -165,19 +165,19 @@ let routes: Routes = [ children: [ { path: '', - loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) + loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule) }, { path: 'testnet', - loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) + loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule) }, { path: 'testnet4', - loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) + loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule) }, { path: 'signet', - loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) + loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule) }, ], }, @@ -212,7 +212,7 @@ let routes: Routes = [ }, { path: '', - loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), data: { preload: true }, }, ]; @@ -225,16 +225,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { { path: '', pathMatch: 'full', - loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), + loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), data: { preload: true }, }, { path: '', - loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule), + loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule), data: { preload: true }, }, { - path: 'wallet', + path: 'widget/wallet', children: [], component: AddressGroupComponent, data: { @@ -248,7 +248,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: '', - loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), + loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), data: { preload: true }, }, { @@ -260,16 +260,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { { path: '', pathMatch: 'full', - loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), + loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), data: { preload: true }, }, { path: '', - loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule), + loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule), data: { preload: true }, }, { - path: 'wallet', + path: 'widget/wallet', children: [], component: AddressGroupComponent, data: { @@ -281,11 +281,11 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { children: [ { path: '', - loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) + loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule) }, { path: 'testnet', - loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) + loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule) }, ], }, @@ -296,7 +296,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: '', - loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), + loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), data: { preload: true }, }, ]; diff --git a/frontend/src/app/app.module.server.ts b/frontend/src/app/app.module.server.ts index 4149fa593..56096891d 100644 --- a/frontend/src/app/app.module.server.ts +++ b/frontend/src/app/app.module.server.ts @@ -2,11 +2,11 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { ServerModule } from '@angular/platform-server'; -import { ZONE_SERVICE } from './injection-tokens'; +import { ZONE_SERVICE } from '@app/injection-tokens'; import { AppModule } from './app.module'; -import { AppComponent } from './components/app/app.component'; -import { HttpCacheInterceptor } from './services/http-cache.interceptor'; -import { ZoneService } from './services/zone.service'; +import { AppComponent } from '@components/app/app.component'; +import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor'; +import { ZoneService } from '@app/services/zone.service'; @NgModule({ @@ -20,4 +20,4 @@ import { ZoneService } from './services/zone.service'; ], bootstrap: [AppComponent], }) -export class AppServerModule {} \ No newline at end of file +export class AppServerModule {} diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 50bbd88b9..1b764c003 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -2,35 +2,38 @@ import { BrowserModule } from '@angular/platform-browser'; import { ModuleWithProviders, NgModule } from '@angular/core'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ZONE_SERVICE } from './injection-tokens'; +import { ZONE_SERVICE } from '@app/injection-tokens'; import { AppRoutingModule } from './app-routing.module'; -import { AppComponent } from './components/app/app.component'; -import { ElectrsApiService } from './services/electrs-api.service'; -import { StateService } from './services/state.service'; -import { CacheService } from './services/cache.service'; -import { PriceService } from './services/price.service'; -import { EnterpriseService } from './services/enterprise.service'; -import { WebsocketService } from './services/websocket.service'; -import { AudioService } from './services/audio.service'; -import { PreloadService } from './services/preload.service'; -import { SeoService } from './services/seo.service'; -import { OpenGraphService } from './services/opengraph.service'; -import { ZoneService } from './services/zone-shim.service'; -import { SharedModule } from './shared/shared.module'; -import { StorageService } from './services/storage.service'; -import { HttpCacheInterceptor } from './services/http-cache.interceptor'; -import { LanguageService } from './services/language.service'; -import { ThemeService } from './services/theme.service'; -import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe'; -import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe'; -import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe'; -import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe'; -import { AppPreloadingStrategy } from './app.preloading-strategy'; -import { ServicesApiServices } from './services/services-api.service'; +import { AppComponent } from '@components/app/app.component'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; +import { OrdApiService } from '@app/services/ord-api.service'; +import { StateService } from '@app/services/state.service'; +import { CacheService } from '@app/services/cache.service'; +import { PriceService } from '@app/services/price.service'; +import { EnterpriseService } from '@app/services/enterprise.service'; +import { WebsocketService } from '@app/services/websocket.service'; +import { AudioService } from '@app/services/audio.service'; +import { PreloadService } from '@app/services/preload.service'; +import { SeoService } from '@app/services/seo.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; +import { ZoneService } from '@app/services/zone-shim.service'; +import { SharedModule } from '@app/shared/shared.module'; +import { StorageService } from '@app/services/storage.service'; +import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor'; +import { LanguageService } from '@app/services/language.service'; +import { ThemeService } from '@app/services/theme.service'; +import { TimeService } from '@app/services/time.service'; +import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe'; +import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe'; +import { ShortenStringPipe } from '@app/shared/pipes/shorten-string-pipe/shorten-string.pipe'; +import { CapAddressPipe } from '@app/shared/pipes/cap-address-pipe/cap-address-pipe'; +import { AppPreloadingStrategy } from '@app/app.preloading-strategy'; +import { ServicesApiServices } from '@app/services/services-api.service'; import { DatePipe } from '@angular/common'; const providers = [ ElectrsApiService, + OrdApiService, StateService, CacheService, PriceService, @@ -42,6 +45,7 @@ const providers = [ EnterpriseService, LanguageService, ThemeService, + TimeService, ShortenStringPipe, FiatShortenerPipe, FiatCurrencyPipe, diff --git a/frontend/src/app/bitcoin-graphs.module.ts b/frontend/src/app/bitcoin-graphs.module.ts index 710743245..f5b1557b1 100644 --- a/frontend/src/app/bitcoin-graphs.module.ts +++ b/frontend/src/app/bitcoin-graphs.module.ts @@ -1,13 +1,13 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Routes, RouterModule } from '@angular/router'; -import { MasterPageComponent } from './components/master-page/master-page.component'; +import { MasterPageComponent } from '@components/master-page/master-page.component'; const routes: Routes = [ { path: '', component: MasterPageComponent, - loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule), + loadChildren: () => import('@app/graphs/graphs.module').then(m => m.GraphsModule), data: { preload: true }, } ]; diff --git a/frontend/src/app/bitcoin.utils.ts b/frontend/src/app/bitcoin.utils.ts index ae522121c..b949cde3c 100644 --- a/frontend/src/app/bitcoin.utils.ts +++ b/frontend/src/app/bitcoin.utils.ts @@ -1,5 +1,5 @@ -import { Transaction, Vin } from './interfaces/electrs.interface'; -import { Hash } from './shared/sha256'; +import { Transaction, Vin } from '@interfaces/electrs.interface'; +import { Hash } from '@app/shared/sha256'; const P2SH_P2WPKH_COST = 21 * 4; // the WU cost for the non-witness part of P2SH-P2WPKH const P2SH_P2WSH_COST = 35 * 4; // the WU cost for the non-witness part of P2SH-P2WSH @@ -303,4 +303,4 @@ export async function calcScriptHash$(script: string): Promise { return hashArray .map((bytes) => bytes.toString(16).padStart(2, '0')) .join(''); -} \ No newline at end of file +} diff --git a/frontend/src/app/components/about/about-sponsors.component.ts b/frontend/src/app/components/about/about-sponsors.component.ts index 6a47c3bd4..f42944173 100644 --- a/frontend/src/app/components/about/about-sponsors.component.ts +++ b/frontend/src/app/components/about/about-sponsors.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -import { EnterpriseService } from '../../services/enterprise.service'; +import { EnterpriseService } from '@app/services/enterprise.service'; @Component({ selector: 'app-about-sponsors', diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 406835572..40d6e1914 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -201,12 +201,17 @@ Leather + + + + Taproot Wizards +
-
+

Whale Sponsors

diff --git a/frontend/src/app/components/about/about.component.scss b/frontend/src/app/components/about/about.component.scss index 6a20239cc..6a76bf299 100644 --- a/frontend/src/app/components/about/about.component.scss +++ b/frontend/src/app/components/about/about.component.scss @@ -92,6 +92,13 @@ } } + .whale-sponsor { + img { + width: 70px; + height: 70px; + } + } + .alliances { margin-bottom: 100px; a { diff --git a/frontend/src/app/components/about/about.component.ts b/frontend/src/app/components/about/about.component.ts index 44bee5828..5963c371c 100644 --- a/frontend/src/app/components/about/about.component.ts +++ b/frontend/src/app/components/about/about.component.ts @@ -1,16 +1,16 @@ import { ChangeDetectionStrategy, Component, ElementRef, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core'; -import { WebsocketService } from '../../services/websocket.service'; -import { SeoService } from '../../services/seo.service'; -import { OpenGraphService } from '../../services/opengraph.service'; -import { StateService } from '../../services/state.service'; +import { WebsocketService } from '@app/services/websocket.service'; +import { SeoService } from '@app/services/seo.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; +import { StateService } from '@app/services/state.service'; import { Observable } from 'rxjs'; -import { ApiService } from '../../services/api.service'; -import { IBackendInfo } from '../../interfaces/websocket.interface'; +import { ApiService } from '@app/services/api.service'; +import { IBackendInfo } from '@interfaces/websocket.interface'; import { Router, ActivatedRoute } from '@angular/router'; import { map, share, tap } from 'rxjs/operators'; -import { ITranslators } from '../../interfaces/node-api.interface'; +import { ITranslators } from '@interfaces/node-api.interface'; import { DOCUMENT } from '@angular/common'; -import { EnterpriseService } from '../../services/enterprise.service'; +import { EnterpriseService } from '@app/services/enterprise.service'; @Component({ selector: 'app-about', diff --git a/frontend/src/app/components/about/about.module.ts b/frontend/src/app/components/about/about.module.ts index 7e8ed42d0..8324876b1 100644 --- a/frontend/src/app/components/about/about.module.ts +++ b/frontend/src/app/components/about/about.module.ts @@ -1,9 +1,9 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Routes, RouterModule } from '@angular/router'; -import { AboutComponent } from './about.component'; -import { AboutSponsorsComponent } from './about-sponsors.component'; -import { SharedModule } from '../../shared/shared.module'; +import { AboutComponent } from '@components/about/about.component'; +import { AboutSponsorsComponent } from '@components/about/about-sponsors.component'; +import { SharedModule } from '@app/shared/shared.module'; const routes: Routes = [ { diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 6b1eadf7d..9d2d2ad46 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -1,16 +1,16 @@ /* eslint-disable no-console */ import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core'; import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs'; -import { ServicesApiServices } from '../../services/services-api.service'; -import { md5, insecureRandomUUID } from '../../shared/common.utils'; -import { StateService } from '../../services/state.service'; -import { AudioService } from '../../services/audio.service'; -import { ETA, EtaService } from '../../services/eta.service'; -import { Transaction } from '../../interfaces/electrs.interface'; -import { MiningStats } from '../../services/mining.service'; -import { IAuth, AuthServiceMempool } from '../../services/auth.service'; -import { EnterpriseService } from '../../services/enterprise.service'; -import { ApiService } from '../../services/api.service'; +import { ServicesApiServices } from '@app/services/services-api.service'; +import { md5, insecureRandomUUID } from '@app/shared/common.utils'; +import { StateService } from '@app/services/state.service'; +import { AudioService } from '@app/services/audio.service'; +import { ETA, EtaService } from '@app/services/eta.service'; +import { Transaction } from '@interfaces/electrs.interface'; +import { MiningStats } from '@app/services/mining.service'; +import { IAuth, AuthServiceMempool } from '@app/services/auth.service'; +import { EnterpriseService } from '@app/services/enterprise.service'; +import { ApiService } from '@app/services/api.service'; import { isDevMode } from '@angular/core'; export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay'; @@ -75,6 +75,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Output() changeMode = new EventEmitter(); calculating = true; + processing = false; selectedOption: 'wait' | 'accel'; cantPayReason = ''; quoteError = ''; // error fetching estimate or initial data @@ -83,13 +84,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { timePaid: number = 0; // time acceleration requested math = Math; isMobile: boolean = window.innerWidth <= 767.98; - isProdDomain = ['mempool.space', - 'mempool-staging.va1.mempool.space', - 'mempool-staging.fmt.mempool.space', - 'mempool-staging.fra.mempool.space', - 'mempool-staging.tk7.mempool.space', - 'mempool-staging.sg1.mempool.space' - ].indexOf(document.location.hostname) > -1; + isProdDomain = false; private _step: CheckoutStep = 'summary'; simpleMode: boolean = true; @@ -142,6 +137,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { private authService: AuthServiceMempool, private enterpriseService: EnterpriseService, ) { + this.isProdDomain = this.stateService.env.PROD_DOMAINS.indexOf(document.location.hostname) > -1; this.accelerationUUID = insecureRandomUUID(); // Check if Apple Pay available @@ -196,9 +192,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy { if (changes.scrollEvent && this.scrollEvent) { this.scrollToElement('acceleratePreviewAnchor', 'start'); } - if (changes.accelerating) { - if ((this.step === 'processing' || this.step === 'paid') && this.accelerating) { + if (changes.accelerating && this.accelerating) { + if (this.step === 'processing' || this.step === 'paid') { this.moveToStep('success'); + } else { // Edge case where the transaction gets accelerated by someone else or on another session + this.closeModal(); } } } @@ -371,6 +369,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.selectFeeRateIndex = index; this.userBid = Math.max(0, fee); this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; + this.validateChoice(); } } @@ -378,9 +377,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy { * Account-based acceleration request */ accelerateWithMempoolAccount(): void { - if (!this.canPay || this.calculating) { + if (!this.canPay || this.calculating || this.processing) { return; } + this.processing = true; if (this.accelerationSubscription) { this.accelerationSubscription.unsubscribe(); } @@ -390,6 +390,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.accelerationUUID ).subscribe({ next: () => { + this.processing = false; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); this.showSuccess = true; @@ -397,6 +398,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.moveToStep('paid'); }, error: (response) => { + this.processing = false; this.accelerateError = response.error; } }); @@ -466,10 +468,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy { * APPLE PAY */ async requestApplePayPayment(): Promise { + if (this.processing) { + return; + } if (this.conversionsSubscription) { this.conversionsSubscription.unsubscribe(); } + this.processing = true; this.conversionsSubscription = this.stateService.conversions$.subscribe( async (conversions) => { this.conversions = conversions; @@ -494,6 +500,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { console.error(`Unable to find apple pay button id='apple-pay-button'`); // Try again setTimeout(this.requestApplePayPayment.bind(this), 500); + this.processing = false; return; } this.loadingApplePay = false; @@ -505,6 +512,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { console.error(`Cannot retreive payment card details`); this.accelerateError = 'apple_pay_no_card_details'; + this.processing = false; return; } const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); @@ -513,9 +521,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy { tokenResult.token, cardTag, `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, - this.accelerationUUID + this.accelerationUUID, + costUSD ).subscribe({ next: () => { + this.processing = false; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); if (this.applePay) { @@ -526,6 +536,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { }, 1000); }, error: (response) => { + this.processing = false; this.accelerateError = response.error; if (!(response.status === 403 && response.error === 'not_available')) { setTimeout(() => { @@ -537,6 +548,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } }); } else { + this.processing = false; let errorMessage = `Tokenization failed with status: ${tokenResult.status}`; if (tokenResult.errors) { errorMessage += ` and errors: ${JSON.stringify( @@ -547,6 +559,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } }); } catch (e) { + this.processing = false; console.error(e); } } @@ -557,10 +570,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy { * GOOGLE PAY */ async requestGooglePayPayment(): Promise { + if (this.processing) { + return; + } if (this.conversionsSubscription) { this.conversionsSubscription.unsubscribe(); } - + + this.processing = true; this.conversionsSubscription = this.stateService.conversions$.subscribe( async (conversions) => { this.conversions = conversions; @@ -595,6 +612,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { console.error(`Cannot retreive payment card details`); this.accelerateError = 'apple_pay_no_card_details'; + this.processing = false; return; } const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); @@ -603,9 +621,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy { tokenResult.token, cardTag, `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, - this.accelerationUUID + this.accelerationUUID, + costUSD ).subscribe({ next: () => { + this.processing = false; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); if (this.googlePay) { @@ -616,6 +636,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { }, 1000); }, error: (response) => { + this.processing = false; this.accelerateError = response.error; if (!(response.status === 403 && response.error === 'not_available')) { setTimeout(() => { @@ -627,6 +648,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } }); } else { + this.processing = false; let errorMessage = `Tokenization failed with status: ${tokenResult.status}`; if (tokenResult.errors) { errorMessage += ` and errors: ${JSON.stringify( @@ -644,10 +666,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy { * CASHAPP */ async requestCashAppPayment(): Promise { + if (this.processing) { + return; + } if (this.conversionsSubscription) { this.conversionsSubscription.unsubscribe(); } + this.processing = true; this.conversionsSubscription = this.stateService.conversions$.subscribe( async (conversions) => { this.conversions = conversions; @@ -678,6 +704,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.cashAppPay.addEventListener('ontokenization', event => { const { tokenResult, error } = event.detail; if (error) { + this.processing = false; this.accelerateError = error; } else if (tokenResult.status === 'OK') { this.servicesApiService.accelerateWithCashApp$( @@ -685,9 +712,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy { tokenResult.token, tokenResult.details.cashAppPay.cashtag, tokenResult.details.cashAppPay.referenceId, - this.accelerationUUID + this.accelerationUUID, + costUSD ).subscribe({ next: () => { + this.processing = false; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); if (this.cashAppPay) { @@ -702,6 +731,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { }, 1000); }, error: (response) => { + this.processing = false; this.accelerateError = response.error; if (!(response.status === 403 && response.error === 'not_available')) { setTimeout(() => { diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.html index a5e258210..564ee0ad1 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.html @@ -12,7 +12,7 @@

- {{ bar.class === 'tx' ? '' : '+' }}{{ bar.fee | number }} sat + {{ bar.class === 'tx' ? '' : '+' }}{{ bar.fee | number }} sats
diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.ts index 393add6ca..5890e6582 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.ts @@ -1,6 +1,6 @@ import { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core'; -import { Transaction } from '../../interfaces/electrs.interface'; -import { AccelerationEstimate, RateOption } from './accelerate-checkout.component'; +import { Transaction } from '@interfaces/electrs.interface'; +import { AccelerationEstimate, RateOption } from '@components/accelerate-checkout/accelerate-checkout.component'; interface GraphBar { rate: number; diff --git a/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.html b/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.html index 67f6cb80e..0f436f9ac 100644 --- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.html +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.html @@ -21,14 +21,14 @@ Fee - {{ accelerationInfo.fee | number }} sat + {{ accelerationInfo.fee | number }} sats Out-of-band fees @if (accelerationInfo.status === 'accelerated') { - {{ accelerationInfo.feeDelta | number }} sat + {{ accelerationInfo.feeDelta | number }} sats } @else { - {{ accelerationInfo.bidBoost | number }} sat + {{ accelerationInfo.bidBoost | number }} sats } @@ -47,13 +47,14 @@ Accelerated by - + +
diff --git a/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.scss b/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.scss index 98a42f0e7..a8c4cd5cf 100644 --- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.scss +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.scss @@ -23,6 +23,7 @@ .label { padding-right: 30px; + vertical-align: top; } .pool-logo { @@ -30,7 +31,8 @@ height: 22px; position: relative; top: -1px; - margin-right: 3px; + margin-right: 4px; + margin-bottom: 4px; } .oobFees { diff --git a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html index 560e54629..ef3ace5ea 100644 --- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html @@ -9,7 +9,7 @@
@if (eta) { - ~ + ~ }
@@ -38,7 +38,7 @@
- +
@@ -46,10 +46,8 @@
@if (tx.status.confirmed) {
- +
- } @else if (standardETA && !tx.status.confirmed) { - }
diff --git a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts index da0eee4a3..728992212 100644 --- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts @@ -1,8 +1,8 @@ import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core'; -import { ETA } from '../../services/eta.service'; -import { Transaction } from '../../interfaces/electrs.interface'; -import { Acceleration, SinglePoolStats } from '../../interfaces/node-api.interface'; -import { MiningService } from '../../services/mining.service'; +import { ETA } from '@app/services/eta.service'; +import { Transaction } from '@interfaces/electrs.interface'; +import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface'; +import { MiningService } from '@app/services/mining.service'; @Component({ selector: 'app-acceleration-timeline', @@ -11,19 +11,16 @@ import { MiningService } from '../../services/mining.service'; }) export class AccelerationTimelineComponent implements OnInit, OnChanges { @Input() transactionTime: number; + @Input() acceleratedAt: number; @Input() tx: Transaction; @Input() accelerationInfo: Acceleration; @Input() eta: ETA; - // A mined transaction has standard ETA and accelerated ETA undefined - // A transaction in mempool has either standardETA defined (if accelerated) or acceleratedETA defined (if not accelerated yet) - @Input() standardETA: number; - @Input() acceleratedETA: number; - acceleratedAt: number; now: number; accelerateRatio: number; useAbsoluteTime: boolean = false; - interval: number; + firstSeenToAccelerated: number; + acceleratedToMined: number; tooltipPosition = null; hoverInfo: any = null; @@ -34,38 +31,24 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges { ) {} ngOnInit(): void { - this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000; - this.now = Math.floor(new Date().getTime() / 1000); - this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600; + this.updateTimes(); this.miningService.getPools().subscribe(pools => { for (const pool of pools) { this.poolsData[pool.unique_id] = pool; } }); - - this.interval = window.setInterval(() => { - this.now = Math.floor(new Date().getTime() / 1000); - this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600; - }, 60000); } ngOnChanges(changes): void { - // Hide standard ETA while we don't have a proper standard ETA calculation, see https://github.com/mempool/mempool/issues/65 - - // if (changes?.eta?.currentValue || changes?.standardETA?.currentValue || changes?.acceleratedETA?.currentValue) { - // if (changes?.eta?.currentValue) { - // if (changes?.acceleratedETA?.currentValue) { - // this.accelerateRatio = Math.floor((Math.floor(changes.eta.currentValue.time / 1000) - this.now) / (Math.floor(changes.acceleratedETA.currentValue / 1000) - this.now)); - // } else if (changes?.standardETA?.currentValue) { - // this.accelerateRatio = Math.floor((Math.floor(changes.standardETA.currentValue / 1000) - this.now) / (Math.floor(changes.eta.currentValue.time / 1000) - this.now)); - // } - // } - // } + this.updateTimes(); } - ngOnDestroy(): void { - clearInterval(this.interval); + 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); } onHover(event, status: string): void { diff --git a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts index d78b663a4..6a99edbf1 100644 --- a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts +++ b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts @@ -1,18 +1,18 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; -import { EChartsOption } from '../../../graphs/echarts'; +import { EChartsOption } from '@app/graphs/echarts'; import { Observable, Subject, Subscription, combineLatest, fromEvent, merge, share } from 'rxjs'; import { startWith, switchMap, tap } from 'rxjs/operators'; -import { SeoService } from '../../../services/seo.service'; +import { SeoService } from '@app/services/seo.service'; import { formatNumber } from '@angular/common'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../../shared/graphs.utils'; -import { StorageService } from '../../../services/storage.service'; -import { MiningService } from '../../../services/mining.service'; +import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '@app/shared/graphs.utils'; +import { StorageService } from '@app/services/storage.service'; +import { MiningService } from '@app/services/mining.service'; import { ActivatedRoute, Router } from '@angular/router'; -import { Acceleration } from '../../../interfaces/node-api.interface'; -import { ServicesApiServices } from '../../../services/services-api.service'; -import { StateService } from '../../../services/state.service'; -import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe'; +import { Acceleration } from '@interfaces/node-api.interface'; +import { ServicesApiServices } from '@app/services/services-api.service'; +import { StateService } from '@app/services/state.service'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; @Component({ selector: 'app-acceleration-fees-graph', @@ -264,7 +264,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest type: 'bar', barWidth: '90%', large: true, - barMinHeight: 1, + barMinHeight: 3, }, ], dataZoom: (this.widget || data.length === 0 )? undefined : [{ diff --git a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts index 392f1392b..65a1e4eb5 100644 --- a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts +++ b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; -import { ServicesApiServices } from '../../../services/services-api.service'; +import { ServicesApiServices } from '@app/services/services-api.service'; export type AccelerationStats = { totalRequested: number; diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html index 8bdd4f14d..5ac288b2e 100644 --- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html +++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html @@ -33,7 +33,7 @@ - {{ (acceleration.feeDelta) | number }} sat + {{ (acceleration.feeDelta) | number }} sats @@ -41,7 +41,7 @@ - {{ acceleration.boost | number }} sat + {{ acceleration.boost | number }} sats ~ @@ -64,7 +64,7 @@ Pending Completed ⌛ Mined ⌛ - Failed ⌛ + Canceled ⌛ diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts index e45a983e1..ee5303530 100644 --- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts +++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts @@ -1,12 +1,12 @@ 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 { Acceleration, BlockExtended, SinglePoolStats } from '../../../interfaces/node-api.interface'; -import { StateService } from '../../../services/state.service'; -import { WebsocketService } from '../../../services/websocket.service'; -import { ServicesApiServices } from '../../../services/services-api.service'; -import { SeoService } from '../../../services/seo.service'; +import { BehaviorSubject, Observable, Subscription, catchError, combineLatest, filter, of, switchMap, tap, throttleTime, timer } from 'rxjs'; +import { Acceleration, BlockExtended, SinglePoolStats } from '@interfaces/node-api.interface'; +import { StateService } from '@app/services/state.service'; +import { WebsocketService } from '@app/services/websocket.service'; +import { ServicesApiServices } from '@app/services/services-api.service'; +import { SeoService } from '@app/services/seo.service'; import { ActivatedRoute, Router } from '@angular/router'; -import { MiningService } from '../../../services/mining.service'; +import { MiningService } from '@app/services/mining.service'; @Component({ selector: 'app-accelerations-list', @@ -61,8 +61,11 @@ export class AccelerationsListComponent implements OnInit, OnDestroy { this.websocketService.want(['blocks']); this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`); - this.paramSubscription = this.route.params.pipe( - tap(params => { + this.paramSubscription = combineLatest([ + this.route.params, + timer(0), + ]).pipe( + tap(([params]) => { this.page = +params['page'] || 1; this.pageSubject.next(this.page); }) @@ -148,4 +151,4 @@ export class AccelerationsListComponent implements OnInit, OnDestroy { this.paramSubscription?.unsubscribe(); this.keyNavigationSubscription?.unsubscribe(); } -} \ No newline at end of file +} diff --git a/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts b/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts index d84c6e97c..ab7d9122e 100644 --- a/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts +++ b/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts @@ -1,18 +1,18 @@ import { ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; -import { SeoService } from '../../../services/seo.service'; -import { OpenGraphService } from '../../../services/opengraph.service'; -import { WebsocketService } from '../../../services/websocket.service'; -import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; -import { StateService } from '../../../services/state.service'; +import { SeoService } from '@app/services/seo.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; +import { WebsocketService } from '@app/services/websocket.service'; +import { Acceleration, BlockExtended } from '@interfaces/node-api.interface'; +import { StateService } from '@app/services/state.service'; import { Observable, Subscription, catchError, combineLatest, distinctUntilChanged, map, of, share, switchMap, tap } from 'rxjs'; -import { Color } from '../../block-overview-graph/sprite-types'; -import { hexToColor } from '../../block-overview-graph/utils'; -import TxView from '../../block-overview-graph/tx-view'; -import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '../../../app.constants'; -import { ServicesApiServices } from '../../../services/services-api.service'; -import { detectWebGL } from '../../../shared/graphs.utils'; -import { AudioService } from '../../../services/audio.service'; -import { ThemeService } from '../../../services/theme.service'; +import { Color } from '@components/block-overview-graph/sprite-types'; +import { hexToColor } from '@components/block-overview-graph/utils'; +import TxView from '@components/block-overview-graph/tx-view'; +import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '@app/app.constants'; +import { ServicesApiServices } from '@app/services/services-api.service'; +import { detectWebGL } from '@app/shared/graphs.utils'; +import { AudioService } from '@app/services/audio.service'; +import { ThemeService } from '@app/services/theme.service'; const acceleratedColor: Color = hexToColor('8F5FF6'); const normalColors = defaultMempoolFeeColors.map(hex => hexToColor(hex + '5F')); diff --git a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html index 13d38443e..dbc79fb95 100644 --- a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html +++ b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html @@ -10,10 +10,10 @@
- @if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) { + @if (accelerationInfo?.acceleratedFeeRate && (!effectiveFeeRate || accelerationInfo.acceleratedFeeRate >= effectiveFeeRate)) { } @else { - + }
diff --git a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts index f95bb71c8..739760017 100644 --- a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts +++ b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts @@ -1,8 +1,8 @@ -import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter } from '@angular/core'; -import { Transaction } from '../../../interfaces/electrs.interface'; -import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface'; -import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts'; -import { MiningStats } from '../../../services/mining.service'; +import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter, ChangeDetectorRef } from '@angular/core'; +import { Transaction } from '@interfaces/electrs.interface'; +import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface'; +import { EChartsOption, PieSeriesOption } from '@app/graphs/echarts'; +import { MiningStats } from '@app/services/mining.service'; function lighten(color, p): { r, g, b } { return { @@ -23,7 +23,8 @@ function toRGB({r,g,b}): string { changeDetection: ChangeDetectionStrategy.OnPush, }) export class ActiveAccelerationBox implements OnChanges { - @Input() tx: Transaction; + @Input() acceleratedBy?: number[]; + @Input() effectiveFeeRate?: number; @Input() accelerationInfo: Acceleration; @Input() miningStats: MiningStats; @Input() pools: number[]; @@ -41,10 +42,12 @@ export class ActiveAccelerationBox implements OnChanges { timespan = ''; chartInstance: any = undefined; - constructor() {} + constructor( + private cd: ChangeDetectorRef, + ) {} ngOnChanges(changes: SimpleChanges): void { - const pools = this.pools || this.accelerationInfo?.pools || this.tx.acceleratedBy; + const pools = this.pools || this.accelerationInfo?.pools || this.acceleratedBy; if (pools && this.miningStats) { this.prepareChartOptions(pools); } @@ -73,15 +76,21 @@ export class ActiveAccelerationBox implements OnChanges { acceleratingPools.forEach((poolId, index) => { const pool = pools[poolId]; const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1); + let color = 'white'; + if (index >= firstSignificantPool) { + if (numSignificantPools > 1) { + color = toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / Math.max((numSignificantPools - 1), 1))); + } else { + color = toRGB({ r: 147, g: 57, b: 244 }); + } + } data.push(getDataItem( pool.lastEstimatedHashrate, - index >= firstSignificantPool - ? toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / (numSignificantPools - 1))) - : 'white', + color, `${pool.name} (${poolShare}%)`, true, ) as PieSeriesOption); - }) + }); this.acceleratedByPercentage = ((totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1) + '%'; const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%'; data.push(getDataItem( @@ -132,6 +141,7 @@ export class ActiveAccelerationBox implements OnChanges { } ] }; + this.cd.markForCheck(); } onChartInit(ec) { @@ -144,4 +154,4 @@ export class ActiveAccelerationBox implements OnChanges { onToggleCpfp(): void { this.toggleCpfp.emit(); } -} \ No newline at end of file +} diff --git a/frontend/src/app/components/acceleration/pending-stats/pending-stats.component.ts b/frontend/src/app/components/acceleration/pending-stats/pending-stats.component.ts index 568e60d7e..ed63ad098 100644 --- a/frontend/src/app/components/acceleration/pending-stats/pending-stats.component.ts +++ b/frontend/src/app/components/acceleration/pending-stats/pending-stats.component.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { Observable, of } from 'rxjs'; import { switchMap } from 'rxjs/operators'; -import { Acceleration } from '../../../interfaces/node-api.interface'; -import { StateService } from '../../../services/state.service'; -import { WebsocketService } from '../../../services/websocket.service'; +import { Acceleration } from '@interfaces/node-api.interface'; +import { StateService } from '@app/services/state.service'; +import { WebsocketService } from '@app/services/websocket.service'; @Component({ selector: 'app-pending-stats', diff --git a/frontend/src/app/components/address-graph/address-graph.component.ts b/frontend/src/app/components/address-graph/address-graph.component.ts index 6d40a8ebb..e8762fbec 100644 --- a/frontend/src/app/components/address-graph/address-graph.component.ts +++ b/frontend/src/app/components/address-graph/address-graph.component.ts @@ -1,16 +1,16 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; -import { echarts, EChartsOption } from '../../graphs/echarts'; +import { echarts, EChartsOption } from '@app/graphs/echarts'; import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs'; import { catchError, map, switchMap, tap } from 'rxjs/operators'; -import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface'; -import { ElectrsApiService } from '../../services/electrs-api.service'; -import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; +import { AddressTxSummary, ChainStats } from '@interfaces/electrs.interface'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; +import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe'; import { Router } from '@angular/router'; -import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; -import { StateService } from '../../services/state.service'; -import { PriceService } from '../../services/price.service'; -import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe'; -import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; +import { StateService } from '@app/services/state.service'; +import { PriceService } from '@app/services/price.service'; +import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe'; +import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe'; const periodSeconds = { '1d': (60 * 60 * 24), @@ -83,7 +83,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { ngOnChanges(changes: SimpleChanges): void { this.isLoading = true; - if (!this.address || !this.stats) { + if (!this.addressSummary$ && (!this.address || !this.stats)) { return; } if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) { @@ -144,15 +144,16 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { } prepareChartOptions(summary: AddressTxSummary[]) { - if (!summary || !this.stats) { + if (!summary) { return; } - let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum); + const total = this.stats ? (this.stats.funded_txo_sum - this.stats.spent_txo_sum) : summary.reduce((acc, tx) => acc + tx.value, 0); + let runningTotal = total; const processData = summary.map(d => { - const balance = total; - const fiatBalance = total * d.price / 100_000_000; - total -= d.value; + const balance = runningTotal; + const fiatBalance = runningTotal * d.price / 100_000_000; + runningTotal -= d.value; return { time: d.time * 1000, balance, @@ -172,7 +173,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { this.fiatData = this.fiatData.filter(d => d[0] >= startFiat); } this.data.push( - {value: [now, this.stats.funded_txo_sum - this.stats.spent_txo_sum], symbol: 'none', tooltip: { show: false }} + {value: [now, total], symbol: 'none', tooltip: { show: false }} ); const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0); diff --git a/frontend/src/app/components/address-group/address-group.component.ts b/frontend/src/app/components/address-group/address-group.component.ts index 30bee7543..560308592 100644 --- a/frontend/src/app/components/address-group/address-group.component.ts +++ b/frontend/src/app/components/address-group/address-group.component.ts @@ -1,15 +1,15 @@ import { Component, OnInit, OnDestroy, ChangeDetectorRef, HostListener } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; -import { ElectrsApiService } from '../../services/electrs-api.service'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; import { switchMap, catchError } from 'rxjs/operators'; -import { Address, Transaction } from '../../interfaces/electrs.interface'; -import { WebsocketService } from '../../services/websocket.service'; -import { StateService } from '../../services/state.service'; -import { AudioService } from '../../services/audio.service'; -import { ApiService } from '../../services/api.service'; +import { Address, Transaction } from '@interfaces/electrs.interface'; +import { WebsocketService } from '@app/services/websocket.service'; +import { StateService } from '@app/services/state.service'; +import { AudioService } from '@app/services/audio.service'; +import { ApiService } from '@app/services/api.service'; import { of, Subscription, forkJoin } from 'rxjs'; -import { SeoService } from '../../services/seo.service'; -import { AddressInformation } from '../../interfaces/node-api.interface'; +import { SeoService } from '@app/services/seo.service'; +import { AddressInformation } from '@interfaces/node-api.interface'; @Component({ selector: 'app-address-group', diff --git a/frontend/src/app/components/address-labels/address-labels.component.ts b/frontend/src/app/components/address-labels/address-labels.component.ts index ff3c27240..0669a22e4 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.ts +++ b/frontend/src/app/components/address-labels/address-labels.component.ts @@ -1,7 +1,7 @@ import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core'; -import { Vin, Vout } from '../../interfaces/electrs.interface'; -import { StateService } from '../../services/state.service'; -import { AddressType, AddressTypeInfo } from '../../shared/address-utils'; +import { Vin, Vout } from '@interfaces/electrs.interface'; +import { StateService } from '@app/services/state.service'; +import { AddressType, AddressTypeInfo } from '@app/shared/address-utils'; @Component({ selector: 'app-address-labels', diff --git a/frontend/src/app/components/address-transactions-widget/address-transactions-widget.component.html b/frontend/src/app/components/address-transactions-widget/address-transactions-widget.component.html index c1c999d6f..ea055a96f 100644 --- a/frontend/src/app/components/address-transactions-widget/address-transactions-widget.component.html +++ b/frontend/src/app/components/address-transactions-widget/address-transactions-widget.component.html @@ -12,7 +12,7 @@ - + diff --git a/frontend/src/app/components/address-transactions-widget/address-transactions-widget.component.ts b/frontend/src/app/components/address-transactions-widget/address-transactions-widget.component.ts index 998d269ba..ab9b124c3 100644 --- a/frontend/src/app/components/address-transactions-widget/address-transactions-widget.component.ts +++ b/frontend/src/app/components/address-transactions-widget/address-transactions-widget.component.ts @@ -1,9 +1,9 @@ import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; -import { StateService } from '../../services/state.service'; -import { Address, AddressTxSummary } from '../../interfaces/electrs.interface'; -import { ElectrsApiService } from '../../services/electrs-api.service'; +import { StateService } from '@app/services/state.service'; +import { Address, AddressTxSummary } from '@interfaces/electrs.interface'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; import { Observable, Subscription, catchError, map, of, switchMap, zip } from 'rxjs'; -import { PriceService } from '../../services/price.service'; +import { PriceService } from '@app/services/price.service'; @Component({ selector: 'app-address-transactions-widget', @@ -43,7 +43,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On startAddressSubscription(): void { this.isLoading = true; - if (!this.address || !this.addressInfo) { + if (!this.addressSummary$ && (!this.address || !this.addressInfo)) { return; } this.transactions$ = (this.addressSummary$ || (this.isPubkey @@ -55,7 +55,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On }) )).pipe( map(summary => { - return summary?.slice(0, 6); + return summary?.filter(tx => Math.abs(tx.value) >= 1000000)?.slice(0, 6); }), switchMap(txs => { return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, txs.length < 3, this.currency).pipe( @@ -68,6 +68,12 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On )))); }) ); + + } + + getAmountDigits(value: number): string { + const decimals = Math.max(0, 4 - Math.ceil(Math.log10(Math.abs(value / 100_000_000)))); + return `1.${decimals}-${decimals}`; } ngOnDestroy(): void { diff --git a/frontend/src/app/components/address/address-preview.component.ts b/frontend/src/app/components/address/address-preview.component.ts index 9bc6e967f..bcc328787 100644 --- a/frontend/src/app/components/address/address-preview.component.ts +++ b/frontend/src/app/components/address/address-preview.component.ts @@ -1,16 +1,16 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; -import { ElectrsApiService } from '../../services/electrs-api.service'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; import { switchMap, filter, catchError, map, tap } from 'rxjs/operators'; -import { Address, Transaction } from '../../interfaces/electrs.interface'; -import { StateService } from '../../services/state.service'; -import { OpenGraphService } from '../../services/opengraph.service'; -import { AudioService } from '../../services/audio.service'; -import { ApiService } from '../../services/api.service'; +import { Address, Transaction } from '@interfaces/electrs.interface'; +import { StateService } from '@app/services/state.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; +import { AudioService } from '@app/services/audio.service'; +import { ApiService } from '@app/services/api.service'; import { of, merge, Subscription, Observable } from 'rxjs'; -import { SeoService } from '../../services/seo.service'; -import { seoDescriptionNetwork } from '../../shared/common.utils'; -import { AddressInformation } from '../../interfaces/node-api.interface'; +import { SeoService } from '@app/services/seo.service'; +import { seoDescriptionNetwork } from '@app/shared/common.utils'; +import { AddressInformation } from '@interfaces/node-api.interface'; @Component({ selector: 'app-address-preview', diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index 31dff2fa5..41d8c151f 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -94,6 +94,20 @@
+ +
+
+

Unspent Outputs

+
+
+
+
+ +
+
+
+
+

@@ -103,7 +117,7 @@

- +
diff --git a/frontend/src/app/components/address/address.component.ts b/frontend/src/app/components/address/address.component.ts index 105863a4e..8786f46ee 100644 --- a/frontend/src/app/components/address/address.component.ts +++ b/frontend/src/app/components/address/address.component.ts @@ -1,17 +1,17 @@ import { Component, OnInit, OnDestroy, HostListener } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; -import { ElectrsApiService } from '../../services/electrs-api.service'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; import { switchMap, filter, catchError, map, tap } from 'rxjs/operators'; -import { Address, ChainStats, Transaction, Vin } from '../../interfaces/electrs.interface'; -import { WebsocketService } from '../../services/websocket.service'; -import { StateService } from '../../services/state.service'; -import { AudioService } from '../../services/audio.service'; -import { ApiService } from '../../services/api.service'; -import { of, merge, Subscription, Observable } from 'rxjs'; -import { SeoService } from '../../services/seo.service'; -import { seoDescriptionNetwork } from '../../shared/common.utils'; -import { AddressInformation } from '../../interfaces/node-api.interface'; -import { AddressTypeInfo } from '../../shared/address-utils'; +import { Address, ChainStats, Transaction, Utxo, Vin } from '@interfaces/electrs.interface'; +import { WebsocketService } from '@app/services/websocket.service'; +import { StateService } from '@app/services/state.service'; +import { AudioService } from '@app/services/audio.service'; +import { ApiService } from '@app/services/api.service'; +import { of, merge, Subscription, Observable, forkJoin } from 'rxjs'; +import { SeoService } from '@app/services/seo.service'; +import { seoDescriptionNetwork } from '@app/shared/common.utils'; +import { AddressInformation } from '@interfaces/node-api.interface'; +import { AddressTypeInfo } from '@app/shared/address-utils'; class AddressStats implements ChainStats { address: string; @@ -104,6 +104,7 @@ export class AddressComponent implements OnInit, OnDestroy { addressString: string; isLoadingAddress = true; transactions: Transaction[]; + utxos: Utxo[]; isLoadingTransactions = true; retryLoadMore = false; error: any; @@ -159,6 +160,7 @@ export class AddressComponent implements OnInit, OnDestroy { this.address = null; this.isLoadingTransactions = true; this.transactions = null; + this.utxos = null; this.addressInfo = null; this.exampleChannel = null; document.body.scrollTo(0, 0); @@ -212,11 +214,23 @@ export class AddressComponent implements OnInit, OnDestroy { this.updateChainStats(); this.isLoadingAddress = false; this.isLoadingTransactions = true; - return address.is_pubkey + const utxoCount = this.chainStats.utxos + this.mempoolStats.utxos; + return forkJoin([ + address.is_pubkey ? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac') - : this.electrsApiService.getAddressTransactions$(address.address); + : this.electrsApiService.getAddressTransactions$(address.address), + (utxoCount > 2 && utxoCount <= 500 ? (address.is_pubkey + ? this.electrsApiService.getScriptHashUtxos$((address.address.length === 66 ? '21' : '41') + address.address + 'ac') + : this.electrsApiService.getAddressUtxos$(address.address)) : of(null)).pipe( + catchError(() => { + return of(null); + }) + ) + ]); }), - switchMap((transactions) => { + switchMap(([transactions, utxos]) => { + this.utxos = utxos; + this.tempTransactions = transactions; if (transactions.length) { this.lastTransactionTxId = transactions[transactions.length - 1].txid; @@ -309,6 +323,7 @@ export class AddressComponent implements OnInit, OnDestroy { this.transactions = this.transactions.slice(); this.mempoolStats.removeTx(transaction); this.audioService.playSound('magic'); + this.confirmTransaction(tx); } else { if (this.addTransaction(transaction, false)) { this.audioService.playSound('magic'); @@ -334,6 +349,31 @@ export class AddressComponent implements OnInit, OnDestroy { } } + // update utxos in-place + if (this.utxos != null) { + let utxosChanged = false; + for (const vin of transaction.vin) { + const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout); + if (utxoIndex !== -1) { + this.utxos.splice(utxoIndex, 1); + utxosChanged = true; + } + } + for (const [index, vout] of transaction.vout.entries()) { + if (vout.scriptpubkey_address === this.address.address) { + this.utxos.push({ + txid: transaction.txid, + vout: index, + value: vout.value, + status: JSON.parse(JSON.stringify(transaction.status)), + }); + utxosChanged = true; + } + } + if (utxosChanged) { + this.utxos = this.utxos.slice(); + } + } return true; } @@ -346,9 +386,65 @@ export class AddressComponent implements OnInit, OnDestroy { this.transactions.splice(index, 1); this.transactions = this.transactions.slice(); + // update utxos in-place + if (this.utxos != null) { + let utxosChanged = false; + for (const vin of transaction.vin) { + if (vin.prevout?.scriptpubkey_address === this.address.address) { + this.utxos.push({ + txid: vin.txid, + vout: vin.vout, + value: vin.prevout.value, + status: { confirmed: true }, // Assuming the input was confirmed + }); + utxosChanged = true; + } + } + for (const [index, vout] of transaction.vout.entries()) { + if (vout.scriptpubkey_address === this.address.address) { + const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index); + if (utxoIndex !== -1) { + this.utxos.splice(utxoIndex, 1); + utxosChanged = true; + } + } + } + if (utxosChanged) { + this.utxos = this.utxos.slice(); + } + } + return true; } + confirmTransaction(transaction: Transaction): void { + // update utxos in-place + if (this.utxos != null) { + let utxosChanged = false; + for (const vin of transaction.vin) { + if (vin.prevout?.scriptpubkey_address === this.address.address) { + const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout); + if (utxoIndex !== -1) { + this.utxos[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status)); + utxosChanged = true; + } + } + } + for (const [index, vout] of transaction.vout.entries()) { + if (vout.scriptpubkey_address === this.address.address) { + const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index); + if (utxoIndex !== -1) { + this.utxos[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status)); + utxosChanged = true; + } + } + } + if (utxosChanged) { + this.utxos = this.utxos.slice(); + } + } + } + loadMore(): void { if (this.isLoadingTransactions || this.fullyLoaded) { return; diff --git a/frontend/src/app/components/addresses-treemap/addresses-treemap.component.html b/frontend/src/app/components/addresses-treemap/addresses-treemap.component.html new file mode 100644 index 000000000..1c44f9aa3 --- /dev/null +++ b/frontend/src/app/components/addresses-treemap/addresses-treemap.component.html @@ -0,0 +1,10 @@ +
+
+
+
+
+ +
+
+
+
diff --git a/frontend/src/app/components/addresses-treemap/addresses-treemap.component.scss b/frontend/src/app/components/addresses-treemap/addresses-treemap.component.scss new file mode 100644 index 000000000..78510203f --- /dev/null +++ b/frontend/src/app/components/addresses-treemap/addresses-treemap.component.scss @@ -0,0 +1,17 @@ +.node-channels-container { + position: relative; +} + +.loading-spinner { + position: absolute; + top: 0; + left: 0; + right: 0; + width: 100%; + z-index: 100; +} + +.spinner-border { + position: relative; + top: 225px; +} \ No newline at end of file diff --git a/frontend/src/app/components/addresses-treemap/addresses-treemap.component.ts b/frontend/src/app/components/addresses-treemap/addresses-treemap.component.ts new file mode 100644 index 000000000..5ff3cf502 --- /dev/null +++ b/frontend/src/app/components/addresses-treemap/addresses-treemap.component.ts @@ -0,0 +1,150 @@ +import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges } from '@angular/core'; +import { Router } from '@angular/router'; +import { EChartsOption, TreemapSeriesOption } from '@app/graphs/echarts'; +import { lerpColor } from '@app/shared/graphs.utils'; +import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; +import { StateService } from '@app/services/state.service'; +import { Address } from '@interfaces/electrs.interface'; +import { formatNumber } from '@angular/common'; + +@Component({ + selector: 'app-addresses-treemap', + templateUrl: './addresses-treemap.component.html', + styleUrls: ['./addresses-treemap.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AddressesTreemap implements OnChanges { + @Input() addresses: Address[]; + @Input() isLoading: boolean = false; + + chartInstance: any; + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + + constructor( + @Inject(LOCALE_ID) public locale: string, + private lightningApiService: LightningApiService, + private amountShortenerPipe: AmountShortenerPipe, + private zone: NgZone, + private router: Router, + public stateService: StateService, + ) {} + + ngOnChanges(): void { + this.prepareChartOptions(); + } + + prepareChartOptions(): void { + const data = this.addresses.map(address => ({ + address: address.address, + value: address.chain_stats.funded_txo_sum - address.chain_stats.spent_txo_sum, + stats: address.chain_stats, + })); + // only consider visible items for the color gradient + const totalValue = data.reduce((acc, address) => acc + address.value, 0); + const maxTxs = data.filter(address => address.value > (totalValue / 2000)).reduce((max, address) => Math.max(max, address.stats.tx_count), 0); + const dataItems = data.map(address => ({ + ...address, + itemStyle: { + color: lerpColor('#1E88E5', '#D81B60', address.stats.tx_count / maxTxs), + } + })); + this.chartOptions = { + tooltip: { + trigger: 'item', + textStyle: { + align: 'left', + } + }, + series: [ + { + height: 300, + left: 0, + right: 0, + bottom: 0, + top: 0, + roam: false, + type: 'treemap', + data: dataItems, + nodeClick: 'link', + progressive: 100, + tooltip: { + show: true, + backgroundColor: 'rgba(17, 19, 31, 1)', + borderRadius: 4, + shadowColor: 'rgba(0, 0, 0, 0.5)', + textStyle: { + color: '#b1b1b1', + }, + borderColor: '#000', + formatter: (value): string => { + if (!value.data.address) { + return ''; + } + return ` + + + + + + + + + + + + + + + + + + + + + + +
${value.data.address}
Received${this.formatValue(value.data.stats.funded_txo_sum)}
Sent${this.formatValue(value.data.stats.spent_txo_sum)}
Balance${this.formatValue(value.data.stats.funded_txo_sum - value.data.stats.spent_txo_sum)}
Transaction count${value.data.stats.tx_count}
+ `; + } + }, + itemStyle: { + borderColor: 'black', + borderWidth: 1, + }, + breadcrumb: { + show: false, + } + } + ] + }; + } + + formatValue(sats: number): string { + if (sats > 100000000) { + return formatNumber(sats / 100000000, this.locale, '1.2-2') + ' BTC'; + } else { + return this.amountShortenerPipe.transform(sats, 2) + ' sats'; + } + } + + onChartInit(ec: any): void { + this.chartInstance = ec; + + this.chartInstance.on('click', (e) => { + //@ts-ignore + if (!e.data.address) { + return; + } + this.zone.run(() => { + //@ts-ignore + const url = new RelativeUrlPipe(this.stateService).transform(`/address/${e.data.address}`); + this.router.navigate([url]); + }); + }); + } +} diff --git a/frontend/src/app/components/amount-selector/amount-selector.component.html b/frontend/src/app/components/amount-selector/amount-selector.component.html new file mode 100644 index 000000000..a16a24d4f --- /dev/null +++ b/frontend/src/app/components/amount-selector/amount-selector.component.html @@ -0,0 +1,7 @@ +
+ +
diff --git a/frontend/src/app/components/amount-selector/amount-selector.component.scss b/frontend/src/app/components/amount-selector/amount-selector.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/components/amount-selector/amount-selector.component.ts b/frontend/src/app/components/amount-selector/amount-selector.component.ts new file mode 100644 index 000000000..e22542eb3 --- /dev/null +++ b/frontend/src/app/components/amount-selector/amount-selector.component.ts @@ -0,0 +1,36 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { StorageService } from '@app/services/storage.service'; +import { StateService } from '@app/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); + } +} diff --git a/frontend/src/app/components/amount/amount.component.html b/frontend/src/app/components/amount/amount.component.html index b513c89d2..cbbdb2dd9 100644 --- a/frontend/src/app/components/amount/amount.component.html +++ b/frontend/src/app/components/amount/amount.component.html @@ -30,7 +30,7 @@ @if (digitsInfo === '1.8-8') { ‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number }} } @else { - ‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | amountShortener : satoshis < 1000 && satoshis > -1000 ? 0 : 1 }} + ‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | amountShortener : (satoshis < 1000 && satoshis > -1000 ? 0 : 1) : undefined : true }} } sats diff --git a/frontend/src/app/components/amount/amount.component.ts b/frontend/src/app/components/amount/amount.component.ts index 93715f3c0..bf40a7567 100644 --- a/frontend/src/app/components/amount/amount.component.ts +++ b/frontend/src/app/components/amount/amount.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; -import { StateService } from '../../services/state.service'; +import { StateService } from '@app/services/state.service'; import { Observable, Subscription } from 'rxjs'; -import { Price } from '../../services/price.service'; +import { Price } from '@app/services/price.service'; @Component({ selector: 'app-amount', diff --git a/frontend/src/app/components/app/app.component.ts b/frontend/src/app/components/app/app.component.ts index 453276966..365c23972 100644 --- a/frontend/src/app/components/app/app.component.ts +++ b/frontend/src/app/components/app/app.component.ts @@ -1,11 +1,11 @@ import { Location } from '@angular/common'; import { Component, HostListener, OnInit, Inject, LOCALE_ID, HostBinding } from '@angular/core'; import { Router, NavigationEnd } from '@angular/router'; -import { StateService } from '../../services/state.service'; -import { OpenGraphService } from '../../services/opengraph.service'; +import { StateService } from '@app/services/state.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; import { NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap'; -import { ThemeService } from '../../services/theme.service'; -import { SeoService } from '../../services/seo.service'; +import { ThemeService } from '@app/services/theme.service'; +import { SeoService } from '@app/services/seo.service'; @Component({ selector: 'app-root', diff --git a/frontend/src/app/components/asset-circulation/asset-circulation.component.ts b/frontend/src/app/components/asset-circulation/asset-circulation.component.ts index cc09c4809..ab41492b0 100644 --- a/frontend/src/app/components/asset-circulation/asset-circulation.component.ts +++ b/frontend/src/app/components/asset-circulation/asset-circulation.component.ts @@ -1,10 +1,10 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; import { combineLatest, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { moveDec } from '../../bitcoin.utils'; -import { AssetsService } from '../../services/assets.service'; -import { ElectrsApiService } from '../../services/electrs-api.service'; -import { environment } from '../../../environments/environment'; +import { moveDec } from '@app/bitcoin.utils'; +import { AssetsService } from '@app/services/assets.service'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; +import { environment } from '@environments/environment'; @Component({ selector: 'app-asset-circulation', diff --git a/frontend/src/app/components/asset/asset.component.ts b/frontend/src/app/components/asset/asset.component.ts index dd09468cc..30bbd594b 100644 --- a/frontend/src/app/components/asset/asset.component.ts +++ b/frontend/src/app/components/asset/asset.component.ts @@ -1,17 +1,17 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; -import { ElectrsApiService } from '../../services/electrs-api.service'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; import { switchMap, filter, catchError, take } from 'rxjs/operators'; -import { Asset, Transaction } from '../../interfaces/electrs.interface'; -import { WebsocketService } from '../../services/websocket.service'; -import { StateService } from '../../services/state.service'; -import { AudioService } from '../../services/audio.service'; -import { ApiService } from '../../services/api.service'; +import { Asset, Transaction } from '@interfaces/electrs.interface'; +import { WebsocketService } from '@app/services/websocket.service'; +import { StateService } from '@app/services/state.service'; +import { AudioService } from '@app/services/audio.service'; +import { ApiService } from '@app/services/api.service'; import { of, merge, Subscription, combineLatest } from 'rxjs'; -import { SeoService } from '../../services/seo.service'; -import { environment } from '../../../environments/environment'; -import { AssetsService } from '../../services/assets.service'; -import { moveDec } from '../../bitcoin.utils'; +import { SeoService } from '@app/services/seo.service'; +import { environment } from '@environments/environment'; +import { AssetsService } from '@app/services/assets.service'; +import { moveDec } from '@app/bitcoin.utils'; @Component({ selector: 'app-asset', diff --git a/frontend/src/app/components/assets/asset-group/asset-group.component.ts b/frontend/src/app/components/assets/asset-group/asset-group.component.ts index 27e048558..3294eed70 100644 --- a/frontend/src/app/components/assets/asset-group/asset-group.component.ts +++ b/frontend/src/app/components/assets/asset-group/asset-group.component.ts @@ -2,8 +2,8 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { combineLatest, Observable } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; -import { ApiService } from '../../../services/api.service'; -import { AssetsService } from '../../../services/assets.service'; +import { ApiService } from '@app/services/api.service'; +import { AssetsService } from '@app/services/assets.service'; @Component({ selector: 'app-asset-group', diff --git a/frontend/src/app/components/assets/assets-featured/assets-featured.component.ts b/frontend/src/app/components/assets/assets-featured/assets-featured.component.ts index a9bf305f6..de6a0e524 100644 --- a/frontend/src/app/components/assets/assets-featured/assets-featured.component.ts +++ b/frontend/src/app/components/assets/assets-featured/assets-featured.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; -import { ApiService } from '../../../services/api.service'; -import { StateService } from '../../../services/state.service'; +import { ApiService } from '@app/services/api.service'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-assets-featured', diff --git a/frontend/src/app/components/assets/assets-nav/assets-nav.component.ts b/frontend/src/app/components/assets/assets-nav/assets-nav.component.ts index c9b044b34..fb280631a 100644 --- a/frontend/src/app/components/assets/assets-nav/assets-nav.component.ts +++ b/frontend/src/app/components/assets/assets-nav/assets-nav.component.ts @@ -4,12 +4,12 @@ import { Router } from '@angular/router'; import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap'; import { merge, Observable, of, Subject } from 'rxjs'; import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; -import { AssetExtended } from '../../../interfaces/electrs.interface'; -import { AssetsService } from '../../../services/assets.service'; -import { SeoService } from '../../../services/seo.service'; -import { StateService } from '../../../services/state.service'; -import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe'; -import { environment } from '../../../../environments/environment'; +import { AssetExtended } from '@interfaces/electrs.interface'; +import { AssetsService } from '@app/services/assets.service'; +import { SeoService } from '@app/services/seo.service'; +import { StateService } from '@app/services/state.service'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; +import { environment } from '@environments/environment'; @Component({ selector: 'app-assets-nav', diff --git a/frontend/src/app/components/assets/assets.component.ts b/frontend/src/app/components/assets/assets.component.ts index 85d236bca..6a573fcd6 100644 --- a/frontend/src/app/components/assets/assets.component.ts +++ b/frontend/src/app/components/assets/assets.component.ts @@ -1,13 +1,13 @@ import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; -import { AssetsService } from '../../services/assets.service'; -import { environment } from '../../../environments/environment'; +import { AssetsService } from '@app/services/assets.service'; +import { environment } from '@environments/environment'; import { UntypedFormGroup } from '@angular/forms'; import { filter, map, switchMap, take } from 'rxjs/operators'; import { ActivatedRoute, Router } from '@angular/router'; import { combineLatest, Observable } from 'rxjs'; -import { AssetExtended } from '../../interfaces/electrs.interface'; -import { SeoService } from '../../services/seo.service'; -import { StateService } from '../../services/state.service'; +import { AssetExtended } from '@interfaces/electrs.interface'; +import { SeoService } from '@app/services/seo.service'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-assets', diff --git a/frontend/src/app/components/balance-widget/balance-widget.component.html b/frontend/src/app/components/balance-widget/balance-widget.component.html index 4923a2c06..87f14de53 100644 --- a/frontend/src/app/components/balance-widget/balance-widget.component.html +++ b/frontend/src/app/components/balance-widget/balance-widget.component.html @@ -4,10 +4,10 @@
BTC Holdings
- {{ ((addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum) / 100_000_000) | number: '1.2-2' }} BTC + {{ ((total) / 100_000_000) | number: '1.2-2' }} BTC
- +
diff --git a/frontend/src/app/components/balance-widget/balance-widget.component.ts b/frontend/src/app/components/balance-widget/balance-widget.component.ts index 8e1d3f442..bd92a2eb9 100644 --- a/frontend/src/app/components/balance-widget/balance-widget.component.ts +++ b/frontend/src/app/components/balance-widget/balance-widget.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; -import { StateService } from '../../services/state.service'; -import { Address, AddressTxSummary } from '../../interfaces/electrs.interface'; -import { ElectrsApiService } from '../../services/electrs-api.service'; +import { StateService } from '@app/services/state.service'; +import { Address, AddressTxSummary } from '@interfaces/electrs.interface'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; import { Observable, catchError, of } from 'rxjs'; @Component({ @@ -19,6 +19,7 @@ export class BalanceWidgetComponent implements OnInit, OnChanges { isLoading: boolean = true; error: any; + total: number = 0; delta7d: number = 0; delta30d: number = 0; @@ -34,7 +35,7 @@ export class BalanceWidgetComponent implements OnInit, OnChanges { ngOnChanges(changes: SimpleChanges): void { this.isLoading = true; - if (!this.address || !this.addressInfo) { + if (!this.addressSummary$ && (!this.address || !this.addressInfo)) { return; } (this.addressSummary$ || (this.isPubkey @@ -57,6 +58,7 @@ export class BalanceWidgetComponent implements OnInit, OnChanges { calculateStats(summary: AddressTxSummary[]): void { let weekTotal = 0; let monthTotal = 0; + this.total = this.addressInfo ? this.addressInfo.chain_stats.funded_txo_sum - this.addressInfo.chain_stats.spent_txo_sum : summary.reduce((acc, tx) => acc + tx.value, 0); const weekAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (7 * 24 * 60 * 60 * 1000)).getTime()) / 1000; const monthAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (30 * 24 * 60 * 60 * 1000)).getTime()) / 1000; diff --git a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts index 067061678..f931f2c31 100644 --- a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts @@ -4,7 +4,7 @@ import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; import { Subscription, of, timer } from 'rxjs'; import { filter, repeat, retry, switchMap, take, tap } from 'rxjs/operators'; -import { ServicesApiServices } from '../../services/services-api.service'; +import { ServicesApiServices } from '@app/services/services-api.service'; @Component({ selector: 'app-bitcoin-invoice', diff --git a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts index c533626e7..07361ef42 100644 --- a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts +++ b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts @@ -1,17 +1,17 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core'; -import { echarts, EChartsOption } from '../../graphs/echarts'; +import { echarts, EChartsOption } from '@app/graphs/echarts'; import { Observable, combineLatest, of } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from '../../services/api.service'; -import { SeoService } from '../../services/seo.service'; +import { ApiService } from '@app/services/api.service'; +import { SeoService } from '@app/services/seo.service'; import { formatNumber } from '@angular/common'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils'; -import { StorageService } from '../../services/storage.service'; -import { MiningService } from '../../services/mining.service'; -import { selectPowerOfTen } from '../../bitcoin.utils'; -import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; -import { StateService } from '../../services/state.service'; +import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '@app/shared/graphs.utils'; +import { StorageService } from '@app/services/storage.service'; +import { MiningService } from '@app/services/mining.service'; +import { selectPowerOfTen } from '@app/bitcoin.utils'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; +import { StateService } from '@app/services/state.service'; import { ActivatedRoute, Router } from '@angular/router'; @Component({ diff --git a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts index 33e3eb19e..c2dea11aa 100644 --- a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts +++ b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts @@ -1,18 +1,18 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; -import { echarts, EChartsOption } from '../../graphs/echarts'; +import { echarts, EChartsOption } from '@app/graphs/echarts'; import { Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from '../../services/api.service'; -import { SeoService } from '../../services/seo.service'; +import { ApiService } from '@app/services/api.service'; +import { SeoService } from '@app/services/seo.service'; import { formatNumber } from '@angular/common'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { download, formatterXAxis } from '../../shared/graphs.utils'; -import { StorageService } from '../../services/storage.service'; -import { MiningService } from '../../services/mining.service'; +import { download, formatterXAxis } from '@app/shared/graphs.utils'; +import { StorageService } from '@app/services/storage.service'; +import { MiningService } from '@app/services/mining.service'; import { ActivatedRoute } from '@angular/router'; -import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe'; -import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe'; -import { StateService } from '../../services/state.service'; +import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe'; +import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-block-fees-graph', diff --git a/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.ts b/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.ts index 42d05510f..deba874a7 100644 --- a/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.ts +++ b/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.ts @@ -1,19 +1,19 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core'; -import { EChartsOption } from '../../graphs/echarts'; +import { EChartsOption } from '@app/graphs/echarts'; import { Observable } from 'rxjs'; import { catchError, map, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from '../../services/api.service'; -import { SeoService } from '../../services/seo.service'; +import { ApiService } from '@app/services/api.service'; +import { SeoService } from '@app/services/seo.service'; import { formatNumber } from '@angular/common'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { download, formatterXAxis } from '../../shared/graphs.utils'; +import { download, formatterXAxis } from '@app/shared/graphs.utils'; import { ActivatedRoute, Router } from '@angular/router'; -import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe'; -import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe'; -import { StateService } from '../../services/state.service'; -import { MiningService } from '../../services/mining.service'; -import { StorageService } from '../../services/storage.service'; -import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; +import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe'; +import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe'; +import { StateService } from '@app/services/state.service'; +import { MiningService } from '@app/services/mining.service'; +import { StorageService } from '@app/services/storage.service'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; @Component({ selector: 'app-block-fees-subsidy-graph', diff --git a/frontend/src/app/components/block-filters/block-filters.component.ts b/frontend/src/app/components/block-filters/block-filters.component.ts index 7f997617c..2a0c0772a 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.ts +++ b/frontend/src/app/components/block-filters/block-filters.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core'; -import { ActiveFilter, FilterGroups, FilterMode, GradientMode, TransactionFilters } from '../../shared/filters.utils'; -import { StateService } from '../../services/state.service'; +import { ActiveFilter, FilterGroups, FilterMode, GradientMode, TransactionFilters } from '@app/shared/filters.utils'; +import { StateService } from '@app/services/state.service'; import { Subscription } from 'rxjs'; @@ -115,4 +115,4 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { ngOnDestroy(): void { this.filterSubscription.unsubscribe(); } -} \ No newline at end of file +} diff --git a/frontend/src/app/components/block-health-graph/block-health-graph.component.ts b/frontend/src/app/components/block-health-graph/block-health-graph.component.ts index 6a7168d6b..8d893a85f 100644 --- a/frontend/src/app/components/block-health-graph/block-health-graph.component.ts +++ b/frontend/src/app/components/block-health-graph/block-health-graph.component.ts @@ -1,16 +1,16 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core'; -import { EChartsOption } from '../../graphs/echarts'; +import { EChartsOption } from '@app/graphs/echarts'; import { Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from '../../services/api.service'; -import { SeoService } from '../../services/seo.service'; +import { ApiService } from '@app/services/api.service'; +import { SeoService } from '@app/services/seo.service'; import { formatNumber } from '@angular/common'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils'; -import { StorageService } from '../../services/storage.service'; +import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '@app/shared/graphs.utils'; +import { StorageService } from '@app/services/storage.service'; import { ActivatedRoute, Router } from '@angular/router'; -import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; -import { StateService } from '../../services/state.service'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-block-health-graph', diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts index 3be0692a5..d59e38c13 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts @@ -1,17 +1,17 @@ import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy, OnChanges } from '@angular/core'; -import { TransactionStripped } from '../../interfaces/node-api.interface'; -import { FastVertexArray } from './fast-vertex-array'; -import BlockScene from './block-scene'; -import TxSprite from './tx-sprite'; -import TxView from './tx-view'; -import { Color, Position } from './sprite-types'; -import { Price } from '../../services/price.service'; -import { StateService } from '../../services/state.service'; -import { ThemeService } from '../../services/theme.service'; +import { TransactionStripped } from '@interfaces/node-api.interface'; +import { FastVertexArray } from '@components/block-overview-graph/fast-vertex-array'; +import BlockScene from '@components/block-overview-graph/block-scene'; +import TxSprite from '@components/block-overview-graph/tx-sprite'; +import TxView from '@components/block-overview-graph/tx-view'; +import { Color, Position } from '@components/block-overview-graph/sprite-types'; +import { Price } from '@app/services/price.service'; +import { StateService } from '@app/services/state.service'; +import { ThemeService } from '@app/services/theme.service'; import { Subscription } from 'rxjs'; -import { defaultColorFunction, setOpacity, defaultAuditColors, defaultColors, ageColorFunction, contrastColorFunction, contrastAuditColors, contrastColors } from './utils'; -import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils'; -import { detectWebGL } from '../../shared/graphs.utils'; +import { defaultColorFunction, setOpacity, defaultAuditColors, defaultColors, ageColorFunction, contrastColorFunction, contrastAuditColors, contrastColors } from '@components/block-overview-graph/utils'; +import { ActiveFilter, FilterMode, toFlags } from '@app/shared/filters.utils'; +import { detectWebGL } from '@app/shared/graphs.utils'; const unmatchedOpacity = 0.2; const unmatchedAuditColors = { diff --git a/frontend/src/app/components/block-overview-graph/block-scene.ts b/frontend/src/app/components/block-overview-graph/block-scene.ts index 4f07818a5..575f45bd6 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -1,9 +1,9 @@ -import { FastVertexArray } from './fast-vertex-array'; -import TxView from './tx-view'; -import { TransactionStripped } from '../../interfaces/node-api.interface'; -import { Color, Position, Square, ViewUpdateParams } from './sprite-types'; -import { defaultColorFunction, contrastColorFunction } from './utils'; -import { ThemeService } from '../../services/theme.service'; +import { FastVertexArray } from '@components/block-overview-graph/fast-vertex-array'; +import TxView from '@components/block-overview-graph/tx-view'; +import { TransactionStripped } from '@interfaces/node-api.interface'; +import { Color, Position, Square, ViewUpdateParams } from '@components/block-overview-graph/sprite-types'; +import { defaultColorFunction, contrastColorFunction } from '@components/block-overview-graph/utils'; +import { ThemeService } from '@app/services/theme.service'; export default class BlockScene { scene: { count: number, offset: { x: number, y: number}}; @@ -917,4 +917,4 @@ class BlockLayout { function feeRateDescending(a: TxView, b: TxView) { return b.feerate - a.feerate; -} \ No newline at end of file +} diff --git a/frontend/src/app/components/block-overview-graph/fast-vertex-array.ts b/frontend/src/app/components/block-overview-graph/fast-vertex-array.ts index bc0900238..42439ef8d 100644 --- a/frontend/src/app/components/block-overview-graph/fast-vertex-array.ts +++ b/frontend/src/app/components/block-overview-graph/fast-vertex-array.ts @@ -8,7 +8,7 @@ or compacting into a smaller Float32Array when there's space to do so. */ -import TxSprite from './tx-sprite'; +import TxSprite from '@components/block-overview-graph/tx-sprite'; export class FastVertexArray { length: number; diff --git a/frontend/src/app/components/block-overview-graph/tx-sprite.ts b/frontend/src/app/components/block-overview-graph/tx-sprite.ts index 75c1577fc..d713cbd77 100644 --- a/frontend/src/app/components/block-overview-graph/tx-sprite.ts +++ b/frontend/src/app/components/block-overview-graph/tx-sprite.ts @@ -1,5 +1,5 @@ -import { FastVertexArray } from './fast-vertex-array'; -import { InterpolatedAttribute, Attributes, OptionalAttributes, SpriteUpdateParams, Update } from './sprite-types'; +import { FastVertexArray } from '@components/block-overview-graph/fast-vertex-array'; +import { InterpolatedAttribute, Attributes, OptionalAttributes, SpriteUpdateParams, Update } from '@components/block-overview-graph/sprite-types'; const attribKeys = ['a', 'b', 't', 'v']; const updateKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a']; diff --git a/frontend/src/app/components/block-overview-graph/tx-view.ts b/frontend/src/app/components/block-overview-graph/tx-view.ts index f612368f4..53ce684ed 100644 --- a/frontend/src/app/components/block-overview-graph/tx-view.ts +++ b/frontend/src/app/components/block-overview-graph/tx-view.ts @@ -1,10 +1,10 @@ -import TxSprite from './tx-sprite'; -import { FastVertexArray } from './fast-vertex-array'; -import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types'; -import { hexToColor } from './utils'; -import BlockScene from './block-scene'; -import { TransactionStripped } from '../../interfaces/node-api.interface'; -import { TransactionFlags } from '../../shared/filters.utils'; +import TxSprite from '@components/block-overview-graph/tx-sprite'; +import { FastVertexArray } from '@components/block-overview-graph/fast-vertex-array'; +import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from '@components/block-overview-graph/sprite-types'; +import { hexToColor } from '@components/block-overview-graph/utils'; +import BlockScene from '@components/block-overview-graph/block-scene'; +import { TransactionStripped } from '@interfaces/node-api.interface'; +import { TransactionFlags } from '@app/shared/filters.utils'; const hoverTransitionTime = 300; const defaultHoverColor = hexToColor('1bd8f4'); diff --git a/frontend/src/app/components/block-overview-graph/utils.ts b/frontend/src/app/components/block-overview-graph/utils.ts index 625029db0..f051e9d51 100644 --- a/frontend/src/app/components/block-overview-graph/utils.ts +++ b/frontend/src/app/components/block-overview-graph/utils.ts @@ -1,6 +1,6 @@ -import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '../../app.constants'; -import { Color } from './sprite-types'; -import TxView from './tx-view'; +import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '@app/app.constants'; +import { Color } from '@components/block-overview-graph/sprite-types'; +import TxView from '@components/block-overview-graph/tx-view'; export function hexToColor(hex: string): Color { return { @@ -11,6 +11,10 @@ export function hexToColor(hex: string): Color { }; } +export function colorToHex(color: Color): string { + return [color.r, color.g, color.b].map(c => Math.round(c * 255).toString(16)).join(''); +} + export function desaturate(color: Color, amount: number): Color { const gray = (color.r + color.g + color.b) / 6; return { @@ -30,6 +34,15 @@ export function darken(color: Color, amount: number): Color { }; } +export function mix(color1: Color, color2: Color, amount: number): Color { + return { + r: color1.r * (1 - amount) + color2.r * amount, + g: color1.g * (1 - amount) + color2.g * amount, + b: color1.b * (1 - amount) + color2.b * amount, + a: color1.a * (1 - amount) + color2.a * amount, + }; +} + export function setOpacity(color: Color, opacity: number): Color { return { ...color, diff --git a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html index f1f5bb3d4..f8fb3c89d 100644 --- a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html +++ b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html @@ -40,7 +40,7 @@ Fee - {{ fee | number }} sat   + {{ fee | number }} sats   Fee rate diff --git a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts index 0a606983e..ffff1b5ed 100644 --- a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts +++ b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts @@ -1,9 +1,9 @@ import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; -import { Position } from '../../components/block-overview-graph/sprite-types.js'; -import { Price } from '../../services/price.service'; -import { TransactionStripped } from '../../interfaces/node-api.interface.js'; -import { Filter, FilterMode, TransactionFlags, toFilters } from '../../shared/filters.utils'; -import { Block } from '../../interfaces/electrs.interface.js'; +import { Position } from '@components/block-overview-graph/sprite-types.js'; +import { Price } from '@app/services/price.service'; +import { TransactionStripped } from '@interfaces/node-api.interface.js'; +import { Filter, FilterMode, TransactionFlags, toFilters } from '@app/shared/filters.utils'; +import { Block } from '@interfaces/electrs.interface.js'; @Component({ selector: 'app-block-overview-tooltip', diff --git a/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.ts b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.ts index 63a543674..15dafb151 100644 --- a/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.ts +++ b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.ts @@ -1,18 +1,18 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; -import { echarts, EChartsOption } from '../../graphs/echarts'; +import { echarts, EChartsOption } from '@app/graphs/echarts'; import { Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from '../../services/api.service'; -import { SeoService } from '../../services/seo.service'; +import { ApiService } from '@app/services/api.service'; +import { SeoService } from '@app/services/seo.service'; import { formatNumber } from '@angular/common'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { download, formatterXAxis } from '../../shared/graphs.utils'; -import { MiningService } from '../../services/mining.service'; -import { StorageService } from '../../services/storage.service'; +import { download, formatterXAxis } from '@app/shared/graphs.utils'; +import { MiningService } from '@app/services/mining.service'; +import { StorageService } from '@app/services/storage.service'; import { ActivatedRoute } from '@angular/router'; -import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe'; -import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe'; -import { StateService } from '../../services/state.service'; +import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe'; +import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-block-rewards-graph', diff --git a/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.ts b/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.ts index b0069dca2..2cc0f0098 100644 --- a/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.ts +++ b/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.ts @@ -1,16 +1,16 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core'; -import { EChartsOption} from '../../graphs/echarts'; +import { EChartsOption} from '@app/graphs/echarts'; import { Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from '../../services/api.service'; -import { SeoService } from '../../services/seo.service'; +import { ApiService } from '@app/services/api.service'; +import { SeoService } from '@app/services/seo.service'; import { formatNumber } from '@angular/common'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { StorageService } from '../../services/storage.service'; -import { MiningService } from '../../services/mining.service'; +import { StorageService } from '@app/services/storage.service'; +import { MiningService } from '@app/services/mining.service'; import { ActivatedRoute } from '@angular/router'; -import { download, formatterXAxis } from '../../shared/graphs.utils'; -import { StateService } from '../../services/state.service'; +import { download, formatterXAxis } from '@app/shared/graphs.utils'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-block-sizes-weights-graph', diff --git a/frontend/src/app/components/block-view/block-view.component.ts b/frontend/src/app/components/block-view/block-view.component.ts index 5c3b7719c..b5d5256ee 100644 --- a/frontend/src/app/components/block-view/block-view.component.ts +++ b/frontend/src/app/components/block-view/block-view.component.ts @@ -1,15 +1,15 @@ import { Component, OnInit, OnDestroy, ViewChild, HostListener } from '@angular/core'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; -import { ElectrsApiService } from '../../services/electrs-api.service'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; import { switchMap, tap, catchError, shareReplay, filter } from 'rxjs/operators'; import { of, Subscription } from 'rxjs'; -import { StateService } from '../../services/state.service'; -import { SeoService } from '../../services/seo.service'; -import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface'; -import { ApiService } from '../../services/api.service'; -import { seoDescriptionNetwork } from '../../shared/common.utils'; -import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component'; -import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; +import { StateService } from '@app/services/state.service'; +import { SeoService } from '@app/services/seo.service'; +import { BlockExtended, TransactionStripped } from '@interfaces/node-api.interface'; +import { ApiService } from '@app/services/api.service'; +import { seoDescriptionNetwork } from '@app/shared/common.utils'; +import { BlockOverviewGraphComponent } from '@components/block-overview-graph/block-overview-graph.component'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; function bestFitResolution(min, max, n): number { const target = (min + max) / 2; diff --git a/frontend/src/app/components/block/block-preview.component.html b/frontend/src/app/components/block/block-preview.component.html index 56fa8886e..036ab8399 100644 --- a/frontend/src/app/components/block/block-preview.component.html +++ b/frontend/src/app/components/block/block-preview.component.html @@ -53,6 +53,13 @@ Miner + + @if (block.extras.pool.minerNames[1].length > 16) { + {{ block.extras.pool.minerNames[1].slice(0, 15) }}… + } @else { + {{ block.extras.pool.minerNames[1] }} + } + {{ block.extras.pool.name }} @@ -60,8 +67,15 @@ - {{ block?.extras.pool.name }} - + + @if (block.extras.pool.minerNames[1].length > 16) { + {{ block.extras.pool.minerNames[1].slice(0, 15) }}… + } @else { + {{ block.extras.pool.minerNames[1] }} + } + + {{ block.extras.pool.name }} + diff --git a/frontend/src/app/components/block/block-preview.component.ts b/frontend/src/app/components/block/block-preview.component.ts index 72da96818..b2fc3fb6f 100644 --- a/frontend/src/app/components/block/block-preview.component.ts +++ b/frontend/src/app/components/block/block-preview.component.ts @@ -1,16 +1,16 @@ import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; -import { ElectrsApiService } from '../../services/electrs-api.service'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; import { switchMap, tap, throttleTime, catchError, shareReplay, startWith, pairwise, filter } from 'rxjs/operators'; import { of, Subscription, asyncScheduler, forkJoin } from 'rxjs'; -import { StateService } from '../../services/state.service'; -import { SeoService } from '../../services/seo.service'; -import { OpenGraphService } from '../../services/opengraph.service'; -import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface'; -import { ApiService } from '../../services/api.service'; -import { seoDescriptionNetwork } from '../../shared/common.utils'; -import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component'; -import { ServicesApiServices } from '../../services/services-api.service'; +import { StateService } from '@app/services/state.service'; +import { SeoService } from '@app/services/seo.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; +import { BlockExtended, TransactionStripped } from '@interfaces/node-api.interface'; +import { ApiService } from '@app/services/api.service'; +import { seoDescriptionNetwork } from '@app/shared/common.utils'; +import { BlockOverviewGraphComponent } from '@components/block-overview-graph/block-overview-graph.component'; +import { ServicesApiServices } from '@app/services/services-api.service'; @Component({ selector: 'app-block-preview', @@ -137,7 +137,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { }) ), this.stateService.env.ACCELERATOR === true && block.height > 819500 - ? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height }) + ? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height }) .pipe(catchError(() => { return of([]); })) diff --git a/frontend/src/app/components/block/block-transactions.component.ts b/frontend/src/app/components/block/block-transactions.component.ts index c0cda6c4f..170d8297d 100644 --- a/frontend/src/app/components/block/block-transactions.component.ts +++ b/frontend/src/app/components/block/block-transactions.component.ts @@ -1,10 +1,10 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { StateService } from '../../services/state.service'; -import { Transaction, Vout } from '../../interfaces/electrs.interface'; +import { StateService } from '@app/services/state.service'; +import { Transaction, Vout } from '@interfaces/electrs.interface'; import { Observable, Subscription, catchError, combineLatest, map, of, startWith, switchMap, tap } from 'rxjs'; import { ActivatedRoute, Router } from '@angular/router'; -import { ElectrsApiService } from '../../services/electrs-api.service'; -import { PreloadService } from '../../services/preload.service'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; +import { PreloadService } from '@app/services/preload.service'; @Component({ selector: 'app-block-transactions', diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index 1dd9d8a8d..105cdf31a 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -66,10 +66,10 @@ [class.badge-success]="blockAudit?.matchRate >= 99" [class.badge-warning]="blockAudit?.matchRate >= 75 && blockAudit?.matchRate < 99" [class.badge-danger]="blockAudit?.matchRate < 75" - *ngIf="blockAudit?.matchRate != null; else nullHealth" + *ngIf="blockAudit?.matchRate != null && blockAudit?.id === block.id; else nullHealth" >{{ blockAudit?.matchRate }}% - + Unknown @@ -182,6 +182,13 @@ Miner + + @if (block.extras.pool.minerNames[1].length > 16) { + {{ block.extras.pool.minerNames[1].slice(0, 15) }}… + } @else { + {{ block.extras.pool.minerNames[1] }} + } + {{ block.extras.pool.name }} diff --git a/frontend/src/app/components/block/block.component.scss b/frontend/src/app/components/block/block.component.scss index fe5318375..945d61366 100644 --- a/frontend/src/app/components/block/block.component.scss +++ b/frontend/src/app/components/block/block.component.scss @@ -81,6 +81,19 @@ h1 { } } +.miner-name { + margin-right: 4px; + vertical-align: top; +} + +.pool-logo { + width: 25px; + height: 25px; + position: relative; + top: -1px; + margin-right: 2px; +} + .row { flex-direction: column; @media (min-width: 768px) { diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index 5cba85e90..dab3c00fa 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -1,23 +1,23 @@ import { Component, OnInit, OnDestroy, ViewChildren, QueryList, ChangeDetectorRef } from '@angular/core'; import { Location } from '@angular/common'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; -import { ElectrsApiService } from '../../services/electrs-api.service'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, filter } from 'rxjs/operators'; import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest, forkJoin } from 'rxjs'; -import { StateService } from '../../services/state.service'; -import { SeoService } from '../../services/seo.service'; -import { WebsocketService } from '../../services/websocket.service'; -import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; -import { Acceleration, BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface'; -import { ApiService } from '../../services/api.service'; -import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component'; -import { detectWebGL } from '../../shared/graphs.utils'; -import { seoDescriptionNetwork } from '../../shared/common.utils'; -import { PriceService, Price } from '../../services/price.service'; -import { CacheService } from '../../services/cache.service'; -import { ServicesApiServices } from '../../services/services-api.service'; -import { PreloadService } from '../../services/preload.service'; -import { identifyPrioritizedTransactions } from '../../shared/transaction.utils'; +import { StateService } from '@app/services/state.service'; +import { SeoService } from '@app/services/seo.service'; +import { WebsocketService } from '@app/services/websocket.service'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; +import { Acceleration, BlockAudit, BlockExtended, TransactionStripped } from '@interfaces/node-api.interface'; +import { ApiService } from '@app/services/api.service'; +import { BlockOverviewGraphComponent } from '@components/block-overview-graph/block-overview-graph.component'; +import { detectWebGL } from '@app/shared/graphs.utils'; +import { seoDescriptionNetwork } from '@app/shared/common.utils'; +import { PriceService, Price } from '@app/services/price.service'; +import { CacheService } from '@app/services/cache.service'; +import { ServicesApiServices } from '@app/services/services-api.service'; +import { PreloadService } from '@app/services/preload.service'; +import { identifyPrioritizedTransactions } from '@app/shared/transaction.utils'; @Component({ selector: 'app-block', @@ -319,7 +319,7 @@ export class BlockComponent implements OnInit, OnDestroy { this.accelerationsSubscription = this.block$.pipe( switchMap((block) => { return this.stateService.env.ACCELERATOR === true && block.height > 819500 - ? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height }) + ? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height }) .pipe(catchError(() => { return of([]); })) @@ -327,7 +327,7 @@ export class BlockComponent implements OnInit, OnDestroy { }) ).subscribe((accelerations) => { this.accelerations = accelerations; - if (accelerations.length) { + if (accelerations.length && this.strippedTransactions) { // Don't call setupBlockAudit if we don't have transactions yet; it will be called later in overviewSubscription this.setupBlockAudit(); } }); @@ -822,4 +822,4 @@ export class BlockComponent implements OnInit, OnDestroy { this.fees = blockReward; } } -} \ No newline at end of file +} diff --git a/frontend/src/app/components/block/block.module.ts b/frontend/src/app/components/block/block.module.ts index 661e52dcf..206492e3f 100644 --- a/frontend/src/app/components/block/block.module.ts +++ b/frontend/src/app/components/block/block.module.ts @@ -1,9 +1,9 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Routes, RouterModule } from '@angular/router'; -import { BlockComponent } from './block.component'; -import { BlockTransactionsComponent } from './block-transactions.component'; -import { SharedModule } from '../../shared/shared.module'; +import { BlockComponent } from '@components/block/block.component'; +import { BlockTransactionsComponent } from '@components/block/block-transactions.component'; +import { SharedModule } from '@app/shared/shared.module'; const routes: Routes = [ { diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html index a60e1db0a..a782e9588 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html @@ -60,9 +60,14 @@
diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss index b8de4f2ca..5c2a5ab5a 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss @@ -19,6 +19,38 @@ pointer-events: none; } +.on-pool-name-text { + display: inline-block; + padding-top: 2px; + font-weight: normal; +} + + +.on-pool { + align-items: center; + background-color: var(--bg); + display: inline-block; + margin-top: 4px; + padding: .25em .4em; + border-radius: .25rem; +} + +.on-pool-container { + align-items: center; + position: relative; + top: -8px; + display: flex; + flex-direction: column; +} + +.on-pool-container.selected { + top: 0px; +} + +.pool-container { + margin-top: 12px; +} + .mined-block { position: absolute; top: 0px; @@ -155,9 +187,16 @@ .badge { position: relative; - top: 15px; + top: 19px; z-index: 101; color: #FFF; + overflow: hidden; + text-overflow: ellipsis; + max-width: 145px; + + &.miner-name { + max-width: 125px; + } } .pool-logo { @@ -168,6 +207,10 @@ margin-right: 2px; } +.pool-logo.faded { + filter: grayscale(100%) brightness(1.5); +} + .animated { transition: all 0.15s ease-in-out; white-space: nowrap; diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts index 1a7598079..008ab1052 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts @@ -1,10 +1,10 @@ import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Observable, Subscription, delay, filter, tap } from 'rxjs'; -import { StateService } from '../../services/state.service'; -import { specialBlocks } from '../../app.constants'; -import { BlockExtended } from '../../interfaces/node-api.interface'; +import { StateService } from '@app/services/state.service'; +import { specialBlocks } from '@app/app.constants'; +import { BlockExtended } from '@interfaces/node-api.interface'; import { Location } from '@angular/common'; -import { CacheService } from '../../services/cache.service'; +import { CacheService } from '@app/services/cache.service'; interface BlockchainBlock extends BlockExtended { placeholder?: boolean; diff --git a/frontend/src/app/components/blockchain/blockchain.component.ts b/frontend/src/app/components/blockchain/blockchain.component.ts index d70e788a2..2e3224a9c 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.ts +++ b/frontend/src/app/components/blockchain/blockchain.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core'; import { firstValueFrom, Subscription } from 'rxjs'; -import { StateService } from '../../services/state.service'; -import { StorageService } from '../../services/storage.service'; +import { StateService } from '@app/services/state.service'; +import { StorageService } from '@app/services/storage.service'; @Component({ selector: 'app-blockchain', diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.html b/frontend/src/app/components/blocks-list/blocks-list.component.html index d82472492..807d429bf 100644 --- a/frontend/src/app/components/blocks-list/blocks-list.component.html +++ b/frontend/src/app/components/blocks-list/blocks-list.component.html @@ -1,8 +1,11 @@
-

Blocks

-
+
+

Blocks

+ +
+
diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.scss b/frontend/src/app/components/blocks-list/blocks-list.component.scss index 2315844ae..9e4465cf1 100644 --- a/frontend/src/app/components/blocks-list/blocks-list.component.scss +++ b/frontend/src/app/components/blocks-list/blocks-list.component.scss @@ -1,7 +1,9 @@ .spinner-border { height: 25px; width: 25px; - margin-top: 13px; + margin-top: -10px; + margin-left: -13px; + flex-shrink: 0; } .container-xl { diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.ts b/frontend/src/app/components/blocks-list/blocks-list.component.ts index 5270ee7be..a5e6486a6 100644 --- a/frontend/src/app/components/blocks-list/blocks-list.component.ts +++ b/frontend/src/app/components/blocks-list/blocks-list.component.ts @@ -2,13 +2,13 @@ import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, I import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, combineLatest, Observable, timer, of, Subscription } from 'rxjs'; import { debounceTime, delayWhen, filter, map, retryWhen, scan, skip, switchMap, tap, throttleTime } from 'rxjs/operators'; -import { BlockExtended } from '../../interfaces/node-api.interface'; -import { ApiService } from '../../services/api.service'; -import { StateService } from '../../services/state.service'; -import { WebsocketService } from '../../services/websocket.service'; -import { SeoService } from '../../services/seo.service'; -import { OpenGraphService } from '../../services/opengraph.service'; -import { seoDescriptionNetwork } from '../../shared/common.utils'; +import { BlockExtended } from '@interfaces/node-api.interface'; +import { ApiService } from '@app/services/api.service'; +import { StateService } from '@app/services/state.service'; +import { WebsocketService } from '@app/services/websocket.service'; +import { SeoService } from '@app/services/seo.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; +import { seoDescriptionNetwork } from '@app/shared/common.utils'; @Component({ selector: 'app-blocks-list', diff --git a/frontend/src/app/components/calculator/calculator.component.html b/frontend/src/app/components/calculator/calculator.component.html index e4ade67d2..e205479ee 100644 --- a/frontend/src/app/components/calculator/calculator.component.html +++ b/frontend/src/app/components/calculator/calculator.component.html @@ -12,7 +12,7 @@
{{ currency$ | async }}
- +
@@ -20,7 +20,7 @@
BTC
- + @@ -28,7 +28,7 @@
sats
- + diff --git a/frontend/src/app/components/calculator/calculator.component.ts b/frontend/src/app/components/calculator/calculator.component.ts index a6f10c049..c276db323 100644 --- a/frontend/src/app/components/calculator/calculator.component.ts +++ b/frontend/src/app/components/calculator/calculator.component.ts @@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { combineLatest, Observable } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; -import { StateService } from '../../services/state.service'; -import { WebsocketService } from '../../services/websocket.service'; +import { StateService } from '@app/services/state.service'; +import { WebsocketService } from '@app/services/websocket.service'; @Component({ selector: 'app-calculator', diff --git a/frontend/src/app/components/clock-face/clock-face.component.ts b/frontend/src/app/components/clock-face/clock-face.component.ts index eec0fa98c..a13594597 100644 --- a/frontend/src/app/components/clock-face/clock-face.component.ts +++ b/frontend/src/app/components/clock-face/clock-face.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'; import { Subscription, tap, timer } from 'rxjs'; -import { StateService } from '../../services/state.service'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-clock-face', diff --git a/frontend/src/app/components/clock/clock.component.ts b/frontend/src/app/components/clock/clock.component.ts index 4a9b19e78..90b3d5d26 100644 --- a/frontend/src/app/components/clock/clock.component.ts +++ b/frontend/src/app/components/clock/clock.component.ts @@ -1,11 +1,11 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Input, OnInit } from '@angular/core'; import { Observable, Subscription, of, switchMap, tap } from 'rxjs'; -import { StateService } from '../../services/state.service'; -import { BlockExtended } from '../../interfaces/node-api.interface'; -import { WebsocketService } from '../../services/websocket.service'; -import { MempoolInfo, Recommendedfees } from '../../interfaces/websocket.interface'; +import { StateService } from '@app/services/state.service'; +import { BlockExtended } from '@interfaces/node-api.interface'; +import { WebsocketService } from '@app/services/websocket.service'; +import { MempoolInfo, Recommendedfees } from '@interfaces/websocket.interface'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; -import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; @Component({ selector: 'app-clock', diff --git a/frontend/src/app/components/clockchain/clockchain.component.ts b/frontend/src/app/components/clockchain/clockchain.component.ts index c17b1e0ae..41faa897b 100644 --- a/frontend/src/app/components/clockchain/clockchain.component.ts +++ b/frontend/src/app/components/clockchain/clockchain.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, OnChanges, ChangeDetectorRef } from '@angular/core'; import { firstValueFrom, Subscription } from 'rxjs'; -import { StateService } from '../../services/state.service'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-clockchain', diff --git a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html index bf72aab69..13f49c5df 100644 --- a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html +++ b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html @@ -257,6 +257,38 @@ } + @case ('walletBalance') { +
+
Treasury
+ +
+ } + @case ('wallet') { + + } + @case ('walletTransactions') { +
+
+
+ +
Treasury Transactions
+
+ +
+
+
+ } @case ('twitter') {
diff --git a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.ts b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.ts index fbaf7be74..59bbc32ef 100644 --- a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.ts +++ b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.ts @@ -1,16 +1,16 @@ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { combineLatest, merge, Observable, of, Subject, Subscription } from 'rxjs'; import { catchError, filter, map, scan, share, shareReplay, startWith, switchMap, tap } from 'rxjs/operators'; -import { BlockExtended, OptimizedMempoolStats, TransactionStripped } from '../../interfaces/node-api.interface'; -import { MempoolInfo, ReplacementInfo } from '../../interfaces/websocket.interface'; -import { ApiService } from '../../services/api.service'; -import { StateService } from '../../services/state.service'; -import { WebsocketService } from '../../services/websocket.service'; -import { SeoService } from '../../services/seo.service'; -import { ActiveFilter, FilterMode, GradientMode, toFlags } from '../../shared/filters.utils'; -import { detectWebGL } from '../../shared/graphs.utils'; -import { Address, AddressTxSummary } from '../../interfaces/electrs.interface'; -import { ElectrsApiService } from '../../services/electrs-api.service'; +import { BlockExtended, OptimizedMempoolStats, TransactionStripped } from '@interfaces/node-api.interface'; +import { MempoolInfo, ReplacementInfo } from '@interfaces/websocket.interface'; +import { ApiService } from '@app/services/api.service'; +import { StateService } from '@app/services/state.service'; +import { WebsocketService } from '@app/services/websocket.service'; +import { SeoService } from '@app/services/seo.service'; +import { ActiveFilter, FilterMode, GradientMode, toFlags } from '@app/shared/filters.utils'; +import { detectWebGL } from '@app/shared/graphs.utils'; +import { Address, AddressTxSummary } from '@interfaces/electrs.interface'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; interface MempoolBlocksData { blocks: number; @@ -62,8 +62,10 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni widgets; addressSubscription: Subscription; + walletSubscription: Subscription; blockTxSubscription: Subscription; addressSummary$: Observable; + walletSummary$: Observable; address: Address; goggleResolution = 82; @@ -107,6 +109,10 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni this.websocketService.stopTrackingAddress(); this.address = null; } + if (this.walletSubscription) { + this.walletSubscription.unsubscribe(); + this.websocketService.stopTrackingWallet(); + } this.destroy$.next(1); this.destroy$.complete(); } @@ -260,6 +266,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni }); this.startAddressSubscription(); + this.startWalletSubscription(); } handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) { @@ -358,6 +365,75 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni } } + startWalletSubscription(): void { + if (this.stateService.env.customize && this.stateService.env.customize.dashboard.widgets.some(w => w.props?.wallet)) { + const walletName = this.stateService.env.customize.dashboard.widgets.find(w => w.props?.wallet).props.wallet; + this.websocketService.startTrackingWallet(walletName); + + this.walletSummary$ = this.apiService.getWallet$(walletName).pipe( + catchError(e => { + return of({}); + }), + switchMap(wallet => this.stateService.walletTransactions$.pipe( + startWith([]), + scan((summaries, newTransactions) => { + const newSummaries: AddressTxSummary[] = []; + for (const tx of newTransactions) { + const funded: Record = {}; + const spent: Record = {}; + const fundedCount: Record = {}; + const spentCount: Record = {}; + for (const vin of tx.vin) { + const address = vin.prevout?.scriptpubkey_address; + if (address && wallet[address]) { + spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0); + spentCount[address] = (spentCount[address] ?? 0) + 1; + } + } + for (const vout of tx.vout) { + const address = vout.scriptpubkey_address; + if (address && wallet[address]) { + funded[address] = (funded[address] ?? 0) + (vout.value ?? 0); + fundedCount[address] = (fundedCount[address] ?? 0) + 1; + } + } + for (const address of Object.keys({ ...funded, ...spent })) { + // add tx to summary + const txSummary: AddressTxSummary = { + txid: tx.txid, + value: (funded[address] ?? 0) - (spent[address] ?? 0), + height: tx.status.block_height, + time: tx.status.block_time, + }; + wallet[address].transactions?.push(txSummary); + newSummaries.push(txSummary); + } + } + return this.deduplicateWalletTransactions([...summaries, ...newSummaries]); + }, this.deduplicateWalletTransactions(Object.values(wallet).flatMap(address => address.transactions))) + )), + share(), + ); + } + } + + deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] { + const transactions = new Map(); + for (const tx of walletTransactions) { + if (transactions.has(tx.txid)) { + transactions.get(tx.txid).value += tx.value; + } else { + transactions.set(tx.txid, tx); + } + } + return Array.from(transactions.values()).sort((a, b) => { + if (a.height === b.height) { + return b.tx_position - a.tx_position; + } + return b.height - a.height; + }); + } + @HostListener('window:resize', ['$event']) onResize(): void { if (window.innerWidth >= 992) { diff --git a/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.components.ts b/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.components.ts index 438a50f74..1257a233a 100644 --- a/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.components.ts +++ b/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.components.ts @@ -1,10 +1,10 @@ import { Component, Inject, LOCALE_ID, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiService } from '../../services/api.service'; +import { ApiService } from '@app/services/api.service'; import { formatNumber } from '@angular/common'; -import { selectPowerOfTen } from '../../bitcoin.utils'; -import { StateService } from '../../services/state.service'; +import { selectPowerOfTen } from '@app/bitcoin.utils'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-difficulty-adjustments-table', diff --git a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts index 90b41d749..84912c8dc 100644 --- a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts +++ b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { combineLatest, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { StateService } from '../../services/state.service'; +import { StateService } from '@app/services/state.service'; interface EpochProgress { base: string; @@ -77,7 +77,7 @@ export class DifficultyMiningComponent implements OnInit { base: `${da.progressPercent.toFixed(2)}%`, change: da.difficultyChange, progress: da.progressPercent, - remainingBlocks: da.remainingBlocks - 1, + remainingBlocks: da.remainingBlocks, colorAdjustments, colorPreviousAdjustments, newDifficultyHeight: da.nextRetargetHeight, diff --git a/frontend/src/app/components/difficulty/difficulty.component.ts b/frontend/src/app/components/difficulty/difficulty.component.ts index 579b49fc3..3737754df 100644 --- a/frontend/src/app/components/difficulty/difficulty.component.ts +++ b/frontend/src/app/components/difficulty/difficulty.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, ElementRef, ViewChild, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; import { combineLatest, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { StateService } from '../..//services/state.service'; +import { StateService } from '@app/services/state.service'; interface EpochProgress { base: string; @@ -153,8 +153,8 @@ export class DifficultyComponent implements OnInit { base: `${da.progressPercent.toFixed(2)}%`, change: da.difficultyChange, progress: da.progressPercent, - minedBlocks: this.currentIndex + 1, - remainingBlocks: da.remainingBlocks - 1, + minedBlocks: this.currentIndex, + remainingBlocks: da.remainingBlocks, expectedBlocks: Math.floor(da.expectedBlocks), colorAdjustments, colorPreviousAdjustments, @@ -247,4 +247,4 @@ function getNextBlockSubsidy(height: number): number { // Subsidy is cut in half every 210,000 blocks which will occur approximately every 4 years. subsidy >>= BigInt(halvings); return Number(subsidy); -} \ No newline at end of file +} diff --git a/frontend/src/app/components/eight-blocks/eight-blocks.component.ts b/frontend/src/app/components/eight-blocks/eight-blocks.component.ts index 81dcc4c5b..8ca8437ac 100644 --- a/frontend/src/app/components/eight-blocks/eight-blocks.component.ts +++ b/frontend/src/app/components/eight-blocks/eight-blocks.component.ts @@ -2,15 +2,15 @@ import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/ import { ActivatedRoute, Router } from '@angular/router'; import { catchError, startWith } from 'rxjs/operators'; import { Subject, Subscription, of } from 'rxjs'; -import { StateService } from '../../services/state.service'; -import { WebsocketService } from '../../services/websocket.service'; -import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; -import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface'; -import { ApiService } from '../../services/api.service'; -import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component'; -import { detectWebGL } from '../../shared/graphs.utils'; +import { StateService } from '@app/services/state.service'; +import { WebsocketService } from '@app/services/websocket.service'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; +import { BlockExtended, TransactionStripped } from '@interfaces/node-api.interface'; +import { ApiService } from '@app/services/api.service'; +import { BlockOverviewGraphComponent } from '@components/block-overview-graph/block-overview-graph.component'; +import { detectWebGL } from '@app/shared/graphs.utils'; import { animate, style, transition, trigger } from '@angular/animations'; -import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe'; +import { BytesPipe } from '@app/shared/pipes/bytes-pipe/bytes.pipe'; function bestFitResolution(min, max, n): number { const target = (min + max) / 2; diff --git a/frontend/src/app/components/faucet/faucet.component.html b/frontend/src/app/components/faucet/faucet.component.html index 0f0307e54..3165ae9a7 100644 --- a/frontend/src/app/components/faucet/faucet.component.html +++ b/frontend/src/app/components/faucet/faucet.component.html @@ -27,6 +27,14 @@
} + @else if (user && user.status === 'pending' && !user.email && user.snsId) { +
+ + + Please verify your account by providing a valid email address. To mitigate spam, we delete unverified accounts at regular intervals. + +
+ } @else if (error === 'not_available') {
diff --git a/frontend/src/app/components/faucet/faucet.component.ts b/frontend/src/app/components/faucet/faucet.component.ts index 566a3b970..33d9a849e 100644 --- a/frontend/src/app/components/faucet/faucet.component.ts +++ b/frontend/src/app/components/faucet/faucet.component.ts @@ -1,13 +1,12 @@ -import { Component, OnDestroy, OnInit, ChangeDetectorRef } from "@angular/core"; -import { FormBuilder, FormGroup, Validators, ValidatorFn, AbstractControl, ValidationErrors } from "@angular/forms"; -import { Subscription } from "rxjs"; -import { StorageService } from "../../services/storage.service"; -import { ServicesApiServices } from "../../services/services-api.service"; -import { getRegex } from "../../shared/regex.utils"; -import { StateService } from "../../services/state.service"; -import { WebsocketService } from "../../services/websocket.service"; -import { AudioService } from "../../services/audio.service"; -import { HttpErrorResponse } from "@angular/common/http"; +import { Component, OnDestroy, OnInit, ChangeDetectorRef } from '@angular/core'; +import { FormBuilder, FormGroup, Validators, ValidatorFn, AbstractControl, ValidationErrors } from '@angular/forms'; +import { Subscription } from 'rxjs'; +import { ServicesApiServices } from '@app/services/services-api.service'; +import { getRegex } from '@app/shared/regex.utils'; +import { StateService } from '@app/services/state.service'; +import { WebsocketService } from '@app/services/websocket.service'; +import { AudioService } from '@app/services/audio.service'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-faucet', @@ -34,7 +33,6 @@ export class FaucetComponent implements OnInit, OnDestroy { constructor( private cd: ChangeDetectorRef, - private storageService: StorageService, private servicesApiService: ServicesApiServices, private formBuilder: FormBuilder, private stateService: StateService, @@ -56,14 +54,17 @@ export class FaucetComponent implements OnInit, OnDestroy { } ngOnInit() { - this.user = this.storageService.getAuth()?.user ?? null; - if (!this.user) { - this.loading = false; - return; - } - - // Setup form - this.updateFaucetStatus(); + this.servicesApiService.userSubject$.subscribe(user => { + this.user = user; + if (!user) { + this.loading = false; + this.cd.markForCheck(); + return; + } + // Setup form + this.updateFaucetStatus(); + this.cd.markForCheck(); + }); // Track transaction this.websocketService.want(['blocks', 'mempool-blocks']); @@ -145,9 +146,6 @@ export class FaucetComponent implements OnInit, OnDestroy { 'address': ['', [Validators.required, Validators.pattern(getRegex('address', 'testnet4')), this.getNotFaucetAddressValidator(faucetAddress)]], 'satoshis': [min, [Validators.required, Validators.min(min), Validators.max(max)]] }); - - this.loading = false; - this.cd.markForCheck(); } updateForm(min, max, faucetAddress: string): void { @@ -160,6 +158,8 @@ export class FaucetComponent implements OnInit, OnDestroy { this.faucetForm.get('satoshis').updateValueAndValidity(); this.faucetForm.get('satoshis').markAsDirty(); } + this.loading = false; + this.cd.markForCheck(); } setAmount(value: number): void { diff --git a/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.ts b/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.ts index c26aae31a..aa57f92d9 100644 --- a/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.ts +++ b/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.ts @@ -1,9 +1,9 @@ import { HostListener, OnChanges, OnDestroy } from '@angular/core'; import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; -import { TransactionStripped } from '../../interfaces/node-api.interface'; -import { StateService } from '../../services/state.service'; -import { VbytesPipe } from '../../shared/pipes/bytes-pipe/vbytes.pipe'; -import { selectPowerOfTen } from '../../bitcoin.utils'; +import { TransactionStripped } from '@interfaces/node-api.interface'; +import { StateService } from '@app/services/state.service'; +import { VbytesPipe } from '@app/shared/pipes/bytes-pipe/vbytes.pipe'; +import { selectPowerOfTen } from '@app/bitcoin.utils'; import { Subscription } from 'rxjs'; @Component({ diff --git a/frontend/src/app/components/fees-box/fees-box.component.ts b/frontend/src/app/components/fees-box/fees-box.component.ts index 78fd102ca..b8689bd3c 100644 --- a/frontend/src/app/components/fees-box/fees-box.component.ts +++ b/frontend/src/app/components/fees-box/fees-box.component.ts @@ -1,10 +1,10 @@ import { Component, OnInit, ChangeDetectionStrategy, OnDestroy, ChangeDetectorRef } from '@angular/core'; -import { StateService } from '../../services/state.service'; +import { StateService } from '@app/services/state.service'; import { Observable, combineLatest, Subscription } from 'rxjs'; -import { Recommendedfees } from '../../interfaces/websocket.interface'; -import { feeLevels } from '../../app.constants'; +import { Recommendedfees } from '@interfaces/websocket.interface'; +import { feeLevels } from '@app/app.constants'; import { map, startWith, tap } from 'rxjs/operators'; -import { ThemeService } from '../../services/theme.service'; +import { ThemeService } from '@app/services/theme.service'; @Component({ selector: 'app-fees-box', diff --git a/frontend/src/app/components/fiat-selector/fiat-selector.component.ts b/frontend/src/app/components/fiat-selector/fiat-selector.component.ts index 732c6e862..a9d4b06a3 100644 --- a/frontend/src/app/components/fiat-selector/fiat-selector.component.ts +++ b/frontend/src/app/components/fiat-selector/fiat-selector.component.ts @@ -1,8 +1,8 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { StorageService } from '../../services/storage.service'; -import { fiatCurrencies } from '../../app.constants'; -import { StateService } from '../../services/state.service'; +import { StorageService } from '@app/services/storage.service'; +import { fiatCurrencies } from '@app/app.constants'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-fiat-selector', diff --git a/frontend/src/app/components/footer/footer.component.ts b/frontend/src/app/components/footer/footer.component.ts index a78d1e195..4001a3875 100644 --- a/frontend/src/app/components/footer/footer.component.ts +++ b/frontend/src/app/components/footer/footer.component.ts @@ -1,8 +1,8 @@ import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; -import { StateService } from '../../services/state.service'; +import { StateService } from '@app/services/state.service'; import { Observable, combineLatest } from 'rxjs'; import { map } from 'rxjs/operators'; -import { MempoolInfo } from '../../interfaces/websocket.interface'; +import { MempoolInfo } from '@interfaces/websocket.interface'; interface MempoolBlocksData { blocks: number; diff --git a/frontend/src/app/components/graphs/graphs.component.ts b/frontend/src/app/components/graphs/graphs.component.ts index d6dcddb2e..c8c620f54 100644 --- a/frontend/src/app/components/graphs/graphs.component.ts +++ b/frontend/src/app/components/graphs/graphs.component.ts @@ -1,6 +1,8 @@ import { Component, OnInit } from '@angular/core'; -import { StateService } from '../../services/state.service'; -import { WebsocketService } from '../../services/websocket.service'; +import { StateService } from '@app/services/state.service'; +import { WebsocketService } from '@app/services/websocket.service'; +import { Router, ActivatedRoute } from '@angular/router'; +import { handleDemoRedirect } from '../../shared/common.utils'; @Component({ selector: 'app-graphs', @@ -13,7 +15,9 @@ export class GraphsComponent implements OnInit { constructor( public stateService: StateService, - private websocketService: WebsocketService + private websocketService: WebsocketService, + private router: Router, + private route: ActivatedRoute ) { } ngOnInit(): void { @@ -22,5 +26,7 @@ export class GraphsComponent implements OnInit { if (this.stateService.env.ACCELERATOR === true && (this.stateService.env.MINING_DASHBOARD === true || this.stateService.env.LIGHTNING === true)) { this.flexWrap = true; } + + handleDemoRedirect(this.route, this.router); } } diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html index b50389ce8..b8a720743 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html @@ -5,7 +5,7 @@
-
Hashrate
+
Hashrate (1w)

{{ hashrates.currentHashrate | amountShortener: 1 : 'H/s' }}

diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts index 0d193514d..d53916b97 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts @@ -1,18 +1,18 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core'; -import { echarts, EChartsOption } from '../../graphs/echarts'; +import { echarts, EChartsOption } from '@app/graphs/echarts'; import { combineLatest, fromEvent, merge, Observable, of } from 'rxjs'; import { map, mergeMap, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from '../../services/api.service'; -import { SeoService } from '../../services/seo.service'; +import { ApiService } from '@app/services/api.service'; +import { SeoService } from '@app/services/seo.service'; import { formatNumber } from '@angular/common'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { selectPowerOfTen } from '../../bitcoin.utils'; -import { StorageService } from '../../services/storage.service'; -import { MiningService } from '../../services/mining.service'; -import { download } from '../../shared/graphs.utils'; +import { selectPowerOfTen } from '@app/bitcoin.utils'; +import { StorageService } from '@app/services/storage.service'; +import { MiningService } from '@app/services/mining.service'; +import { download } from '@app/shared/graphs.utils'; import { ActivatedRoute } from '@angular/router'; -import { StateService } from '../../services/state.service'; -import { seoDescriptionNetwork } from '../../shared/common.utils'; +import { StateService } from '@app/services/state.service'; +import { seoDescriptionNetwork } from '@app/shared/common.utils'; @Component({ selector: 'app-hashrate-chart', diff --git a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts index 3fca15bf3..f93cf460d 100644 --- a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts +++ b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts @@ -1,16 +1,16 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core'; -import { EChartsOption } from '../../graphs/echarts'; +import { EChartsOption } from '@app/graphs/echarts'; import { Observable } from 'rxjs'; import { delay, map, retryWhen, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from '../../services/api.service'; -import { SeoService } from '../../services/seo.service'; +import { ApiService } from '@app/services/api.service'; +import { SeoService } from '@app/services/seo.service'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { chartColors, poolsColor } from '../../app.constants'; -import { StorageService } from '../../services/storage.service'; -import { MiningService } from '../../services/mining.service'; -import { download } from '../../shared/graphs.utils'; +import { chartColors, poolsColor } from '@app/app.constants'; +import { StorageService } from '@app/services/storage.service'; +import { MiningService } from '@app/services/mining.service'; +import { download } from '@app/shared/graphs.utils'; import { ActivatedRoute } from '@angular/router'; -import { StateService } from '../../services/state.service'; +import { StateService } from '@app/services/state.service'; interface Hashrate { timestamp: number; diff --git a/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts b/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts index 3487d6fb0..754d5bdde 100644 --- a/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts +++ b/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts @@ -1,10 +1,10 @@ import { Component, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core'; -import { EChartsOption } from '../../graphs/echarts'; +import { EChartsOption } from '@app/graphs/echarts'; import { OnChanges } from '@angular/core'; -import { StorageService } from '../../services/storage.service'; -import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/graphs.utils'; +import { StorageService } from '@app/services/storage.service'; +import { download, formatterXAxis, formatterXAxisLabel } from '@app/shared/graphs.utils'; import { formatNumber } from '@angular/common'; -import { StateService } from '../../services/state.service'; +import { StateService } from '@app/services/state.service'; import { Subscription } from 'rxjs'; const OUTLIERS_MEDIAN_MULTIPLIER = 4; diff --git a/frontend/src/app/components/language-selector/language-selector.component.ts b/frontend/src/app/components/language-selector/language-selector.component.ts index 2b9e559f0..b6df5599a 100644 --- a/frontend/src/app/components/language-selector/language-selector.component.ts +++ b/frontend/src/app/components/language-selector/language-selector.component.ts @@ -1,8 +1,8 @@ import { DOCUMENT } from '@angular/common'; import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { languages } from '../../app.constants'; -import { LanguageService } from '../../services/language.service'; +import { languages } from '@app/app.constants'; +import { LanguageService } from '@app/services/language.service'; @Component({ selector: 'app-language-selector', diff --git a/frontend/src/app/components/lbtc-pegs-graph/lbtc-pegs-graph.component.ts b/frontend/src/app/components/lbtc-pegs-graph/lbtc-pegs-graph.component.ts index a8ec36bec..063280898 100644 --- a/frontend/src/app/components/lbtc-pegs-graph/lbtc-pegs-graph.component.ts +++ b/frontend/src/app/components/lbtc-pegs-graph/lbtc-pegs-graph.component.ts @@ -1,7 +1,7 @@ import { Component, Inject, LOCALE_ID, ChangeDetectionStrategy, Input, OnChanges, OnInit } from '@angular/core'; import { formatDate, formatNumber } from '@angular/common'; -import { EChartsOption } from '../../graphs/echarts'; -import { StateService } from '../../services/state.service'; +import { EChartsOption } from '@app/graphs/echarts'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-lbtc-pegs-graph', diff --git a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts index 07929d894..be4815f28 100644 --- a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts +++ b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts @@ -1,9 +1,9 @@ import { Component, OnInit } from '@angular/core'; -import { Env, StateService } from '../../services/state.service'; +import { Env, StateService } from '@app/services/state.service'; import { merge, Observable, of} from 'rxjs'; -import { LanguageService } from '../../services/language.service'; -import { EnterpriseService } from '../../services/enterprise.service'; -import { NavigationService } from '../../services/navigation.service'; +import { LanguageService } from '@app/services/language.service'; +import { EnterpriseService } from '@app/services/enterprise.service'; +import { NavigationService } from '@app/services/navigation.service'; @Component({ selector: 'app-liquid-master-page', diff --git a/frontend/src/app/components/liquid-reserves-audit/expired-utxos-stats/expired-utxos-stats.component.ts b/frontend/src/app/components/liquid-reserves-audit/expired-utxos-stats/expired-utxos-stats.component.ts index 90a737275..e9de3cce3 100644 --- a/frontend/src/app/components/liquid-reserves-audit/expired-utxos-stats/expired-utxos-stats.component.ts +++ b/frontend/src/app/components/liquid-reserves-audit/expired-utxos-stats/expired-utxos-stats.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { Observable, map, of } from 'rxjs'; -import { FederationUtxo } from '../../../interfaces/node-api.interface'; +import { FederationUtxo } from '@interfaces/node-api.interface'; @Component({ selector: 'app-expired-utxos-stats', diff --git a/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.ts b/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.ts index caeac1987..e098dfc34 100644 --- a/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.ts +++ b/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.ts @@ -1,10 +1,10 @@ import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; import { Observable, Subject, combineLatest, of, timer } from 'rxjs'; import { delayWhen, filter, map, share, shareReplay, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators'; -import { ApiService } from '../../../services/api.service'; -import { Env, StateService } from '../../../services/state.service'; -import { AuditStatus, CurrentPegs, FederationAddress } from '../../../interfaces/node-api.interface'; -import { WebsocketService } from '../../../services/websocket.service'; +import { ApiService } from '@app/services/api.service'; +import { Env, StateService } from '@app/services/state.service'; +import { AuditStatus, CurrentPegs, FederationAddress } from '@interfaces/node-api.interface'; +import { WebsocketService } from '@app/services/websocket.service'; @Component({ selector: 'app-federation-addresses-list', diff --git a/frontend/src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.ts b/frontend/src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.ts index e41c49643..44d0e44f8 100644 --- a/frontend/src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.ts +++ b/frontend/src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.ts @@ -2,10 +2,10 @@ import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, Observable, Subject, combineLatest, of, timer } from 'rxjs'; import { delayWhen, filter, map, share, shareReplay, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators'; -import { ApiService } from '../../../services/api.service'; -import { Env, StateService } from '../../../services/state.service'; -import { AuditStatus, CurrentPegs, FederationUtxo } from '../../../interfaces/node-api.interface'; -import { WebsocketService } from '../../../services/websocket.service'; +import { ApiService } from '@app/services/api.service'; +import { Env, StateService } from '@app/services/state.service'; +import { AuditStatus, CurrentPegs, FederationUtxo } from '@interfaces/node-api.interface'; +import { WebsocketService } from '@app/services/websocket.service'; @Component({ selector: 'app-federation-utxos-list', diff --git a/frontend/src/app/components/liquid-reserves-audit/federation-wallet/federation-wallet.component.ts b/frontend/src/app/components/liquid-reserves-audit/federation-wallet/federation-wallet.component.ts index 51a4cedc2..1c87a8783 100644 --- a/frontend/src/app/components/liquid-reserves-audit/federation-wallet/federation-wallet.component.ts +++ b/frontend/src/app/components/liquid-reserves-audit/federation-wallet/federation-wallet.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { SeoService } from '../../../services/seo.service'; +import { SeoService } from '@app/services/seo.service'; @Component({ selector: 'app-federation-wallet', diff --git a/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.ts b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.ts index d809f2fa0..f11e03a28 100644 --- a/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.ts +++ b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.ts @@ -2,11 +2,11 @@ import { ActivatedRoute, Router } from '@angular/router'; import { Component, OnInit, ChangeDetectionStrategy, Input, Inject, LOCALE_ID, ChangeDetectorRef } from '@angular/core'; import { BehaviorSubject, Observable, Subject, Subscription, combineLatest, of, timer } from 'rxjs'; import { delayWhen, filter, map, share, shareReplay, switchMap, take, takeUntil, tap, throttleTime } from 'rxjs/operators'; -import { ApiService } from '../../../services/api.service'; -import { Env, StateService } from '../../../services/state.service'; -import { AuditStatus, CurrentPegs, RecentPeg } from '../../../interfaces/node-api.interface'; -import { WebsocketService } from '../../../services/websocket.service'; -import { SeoService } from '../../../services/seo.service'; +import { ApiService } from '@app/services/api.service'; +import { Env, StateService } from '@app/services/state.service'; +import { AuditStatus, CurrentPegs, RecentPeg } from '@interfaces/node-api.interface'; +import { WebsocketService } from '@app/services/websocket.service'; +import { SeoService } from '@app/services/seo.service'; @Component({ selector: 'app-recent-pegs-list', diff --git a/frontend/src/app/components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component.ts b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component.ts index 7bf8e6910..29033b848 100644 --- a/frontend/src/app/components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component.ts +++ b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; -import { PegsVolume } from '../../../interfaces/node-api.interface'; +import { PegsVolume } from '@interfaces/node-api.interface'; @Component({ selector: 'app-recent-pegs-stats', diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.ts b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.ts index 45cd63db0..770940325 100644 --- a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.ts +++ b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.ts @@ -1,6 +1,6 @@ import { Component, ChangeDetectionStrategy, Input, OnChanges, OnInit, HostListener } from '@angular/core'; -import { EChartsOption } from '../../../graphs/echarts'; -import { CurrentPegs } from '../../../interfaces/node-api.interface'; +import { EChartsOption } from '@app/graphs/echarts'; +import { CurrentPegs } from '@interfaces/node-api.interface'; @Component({ diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component.ts b/frontend/src/app/components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component.ts index 61f2deb8c..97d1b3da0 100644 --- a/frontend/src/app/components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component.ts +++ b/frontend/src/app/components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; -import { Env, StateService } from '../../../services/state.service'; -import { CurrentPegs } from '../../../interfaces/node-api.interface'; +import { Env, StateService } from '@app/services/state.service'; +import { CurrentPegs } from '@interfaces/node-api.interface'; @Component({ selector: 'app-reserves-supply-stats', diff --git a/frontend/src/app/components/loading-indicator/loading-indicator.component.ts b/frontend/src/app/components/loading-indicator/loading-indicator.component.ts index 83a5ccc72..9cdb0bd06 100644 --- a/frontend/src/app/components/loading-indicator/loading-indicator.component.ts +++ b/frontend/src/app/components/loading-indicator/loading-indicator.component.ts @@ -1,8 +1,8 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { StateService } from '../../services/state.service'; -import { WebsocketService } from '../../services/websocket.service'; +import { StateService } from '@app/services/state.service'; +import { WebsocketService } from '@app/services/websocket.service'; @Component({ selector: 'app-loading-indicator', diff --git a/frontend/src/app/components/master-page-preview/master-page-preview.component.html b/frontend/src/app/components/master-page-preview/master-page-preview.component.html index 8f3204ec4..01995906f 100644 --- a/frontend/src/app/components/master-page-preview/master-page-preview.component.html +++ b/frontend/src/app/components/master-page-preview/master-page-preview.component.html @@ -6,7 +6,7 @@ } @if (enterpriseInfo?.header_img) { - enterpriseInfo.title + } @else { diff --git a/frontend/src/app/components/master-page-preview/master-page-preview.component.ts b/frontend/src/app/components/master-page-preview/master-page-preview.component.ts index 64bdcfda2..c9db2b143 100644 --- a/frontend/src/app/components/master-page-preview/master-page-preview.component.ts +++ b/frontend/src/app/components/master-page-preview/master-page-preview.component.ts @@ -1,8 +1,8 @@ import { Component, OnInit } from '@angular/core'; -import { StateService } from '../../services/state.service'; +import { StateService } from '@app/services/state.service'; import { Observable, Subscription, merge, of } from 'rxjs'; -import { LanguageService } from '../../services/language.service'; -import { EnterpriseService } from '../../services/enterprise.service'; +import { LanguageService } from '@app/services/language.service'; +import { EnterpriseService } from '@app/services/enterprise.service'; @Component({ selector: 'app-master-page-preview', diff --git a/frontend/src/app/components/master-page-preview/preview-title.component.ts b/frontend/src/app/components/master-page-preview/preview-title.component.ts index a26368c89..07883475b 100644 --- a/frontend/src/app/components/master-page-preview/preview-title.component.ts +++ b/frontend/src/app/components/master-page-preview/preview-title.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { StateService } from '../../services/state.service'; +import { StateService } from '@app/services/state.service'; import { Observable, merge, of } from 'rxjs'; @Component({ diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index 9fc2d4e58..557529eef 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -19,7 +19,7 @@ @if (enterpriseInfo?.header_img) { - enterpriseInfo.title + } @else {
@@ -39,7 +39,7 @@ @if (enterpriseInfo?.header_img) { - enterpriseInfo.title + } @else {
@@ -49,7 +49,7 @@ @if (enterpriseInfo?.header_img) { - enterpriseInfo.title + } @else { @@ -85,7 +85,6 @@
\ No newline at end of file diff --git a/frontend/src/app/components/push-transaction/push-transaction.component.ts b/frontend/src/app/components/push-transaction/push-transaction.component.ts index 03a050dfa..221333edb 100644 --- a/frontend/src/app/components/push-transaction/push-transaction.component.ts +++ b/frontend/src/app/components/push-transaction/push-transaction.component.ts @@ -1,12 +1,13 @@ import { Component, OnInit } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; -import { ApiService } from '../../services/api.service'; -import { StateService } from '../../services/state.service'; -import { SeoService } from '../../services/seo.service'; -import { OpenGraphService } from '../../services/opengraph.service'; -import { seoDescriptionNetwork } from '../../shared/common.utils'; +import { ApiService } from '@app/services/api.service'; +import { StateService } from '@app/services/state.service'; +import { SeoService } from '@app/services/seo.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; +import { seoDescriptionNetwork } from '@app/shared/common.utils'; import { ActivatedRoute, Router } from '@angular/router'; -import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; +import { TxResult } from '@interfaces/node-api.interface'; @Component({ selector: 'app-push-transaction', @@ -19,6 +20,16 @@ export class PushTransactionComponent implements OnInit { txId: string = ''; isLoading = false; + submitTxsForm: UntypedFormGroup; + errorPackage: string = ''; + packageMessage: string = ''; + results: TxResult[] = []; + invalidMaxfeerate = false; + invalidMaxburnamount = false; + isLoadingPackage = false; + + network = this.stateService.network; + constructor( private formBuilder: UntypedFormBuilder, private apiService: ApiService, @@ -35,6 +46,14 @@ export class PushTransactionComponent implements OnInit { txHash: ['', Validators.required], }); + this.submitTxsForm = this.formBuilder.group({ + txs: ['', Validators.required], + maxfeerate: ['', Validators.min(0)], + maxburnamount: ['', Validators.min(0)], + }); + + this.stateService.networkChanged$.subscribe((network) => this.network = network); + this.seoService.setTitle($localize`:@@meta.title.push-tx:Broadcast Transaction`); this.seoService.setDescription($localize`:@@meta.description.push-tx:Broadcast a transaction to the ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} network using the transaction's hash.`); this.ogService.setManualOgImage('tx-push.jpg'); @@ -59,7 +78,7 @@ export class PushTransactionComponent implements OnInit { }, (error) => { if (typeof error.error === 'string') { - const matchText = error.error.match('"message":"(.*?)"'); + const matchText = error.error.replace(/\\/g, '').match('"message":"(.*?)"'); this.error = 'Failed to broadcast transaction, reason: ' + (matchText && matchText[1] || error.error); } else if (error.message) { this.error = 'Failed to broadcast transaction, reason: ' + error.message; @@ -70,6 +89,67 @@ export class PushTransactionComponent implements OnInit { }); } + submitTxs() { + let txs: string[] = []; + try { + txs = (this.submitTxsForm.get('txs')?.value as string).split(',').map(hex => hex.trim()); + if (txs?.length === 1) { + this.pushTxForm.get('txHash').setValue(txs[0]); + this.submitTxsForm.get('txs').setValue(''); + this.postTx(); + return; + } + } catch (e) { + this.errorPackage = e?.message; + return; + } + + let maxfeerate; + let maxburnamount; + this.invalidMaxfeerate = false; + this.invalidMaxburnamount = false; + try { + const maxfeerateVal = this.submitTxsForm.get('maxfeerate')?.value; + if (maxfeerateVal != null && maxfeerateVal !== '') { + maxfeerate = parseFloat(maxfeerateVal) / 100_000; + } + } catch (e) { + this.invalidMaxfeerate = true; + } + try { + const maxburnamountVal = this.submitTxsForm.get('maxburnamount')?.value; + if (maxburnamountVal != null && maxburnamountVal !== '') { + maxburnamount = parseInt(maxburnamountVal) / 100_000_000; + } + } catch (e) { + this.invalidMaxburnamount = true; + } + + this.isLoadingPackage = true; + this.errorPackage = ''; + this.results = []; + this.apiService.submitPackage$(txs, maxfeerate === 0.1 ? null : maxfeerate, maxburnamount === 0 ? null : maxburnamount) + .subscribe((result) => { + this.isLoadingPackage = false; + + this.packageMessage = result['package_msg']; + for (let wtxid in result['tx-results']) { + this.results.push(result['tx-results'][wtxid]); + } + + this.submitTxsForm.reset(); + }, + (error) => { + if (typeof error.error?.error === 'string') { + const matchText = error.error.error.replace(/\\/g, '').match('"message":"(.*?)"'); + this.errorPackage = matchText && matchText[1] || error.error.error; + } else if (error.message) { + this.errorPackage = error.message; + } + this.isLoadingPackage = false; + }); + } + private async handleColdcardPushTx(fragmentParams: URLSearchParams): Promise { // maybe conforms to Coldcard nfc-pushtx spec if (fragmentParams && fragmentParams.get('t')) { diff --git a/frontend/src/app/components/qrcode/qrcode.component.ts b/frontend/src/app/components/qrcode/qrcode.component.ts index f377895c0..061625eed 100644 --- a/frontend/src/app/components/qrcode/qrcode.component.ts +++ b/frontend/src/app/components/qrcode/qrcode.component.ts @@ -1,6 +1,6 @@ import { Component, Input, AfterViewInit, ViewChild, ElementRef, ChangeDetectionStrategy } from '@angular/core'; import * as QRCode from 'qrcode'; -import { StateService } from '../../services/state.service'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-qrcode', diff --git a/frontend/src/app/components/rate-unit-selector/rate-unit-selector.component.ts b/frontend/src/app/components/rate-unit-selector/rate-unit-selector.component.ts index a7d94cec2..5e6b324bf 100644 --- a/frontend/src/app/components/rate-unit-selector/rate-unit-selector.component.ts +++ b/frontend/src/app/components/rate-unit-selector/rate-unit-selector.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { StorageService } from '../../services/storage.service'; -import { StateService } from '../../services/state.service'; +import { StorageService } from '@app/services/storage.service'; +import { StateService } from '@app/services/state.service'; import { Subscription } from 'rxjs'; @Component({ diff --git a/frontend/src/app/components/rbf-list/rbf-list.component.ts b/frontend/src/app/components/rbf-list/rbf-list.component.ts index 25f7dea2e..d835b4a59 100644 --- a/frontend/src/app/components/rbf-list/rbf-list.component.ts +++ b/frontend/src/app/components/rbf-list/rbf-list.component.ts @@ -2,13 +2,13 @@ import { Component, OnInit, ChangeDetectionStrategy, OnDestroy } from '@angular/ import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, EMPTY, merge, Observable, Subscription } from 'rxjs'; import { catchError, switchMap, tap } from 'rxjs/operators'; -import { WebsocketService } from '../../services/websocket.service'; -import { RbfTree } from '../../interfaces/node-api.interface'; -import { ApiService } from '../../services/api.service'; -import { StateService } from '../../services/state.service'; -import { SeoService } from '../../services/seo.service'; -import { OpenGraphService } from '../../services/opengraph.service'; -import { seoDescriptionNetwork } from '../../shared/common.utils'; +import { WebsocketService } from '@app/services/websocket.service'; +import { RbfTree } from '@interfaces/node-api.interface'; +import { ApiService } from '@app/services/api.service'; +import { StateService } from '@app/services/state.service'; +import { SeoService } from '@app/services/seo.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; +import { seoDescriptionNetwork } from '@app/shared/common.utils'; @Component({ selector: 'app-rbf-list', diff --git a/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html b/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html index 46cda0488..19c08bad9 100644 --- a/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html +++ b/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html @@ -19,7 +19,7 @@ Fee - {{ rbfInfo.tx.fee | number }} sat + {{ rbfInfo.tx.fee | number }} sats Virtual size diff --git a/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.ts b/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.ts index fc3748f32..3368eeaf3 100644 --- a/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.ts +++ b/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.ts @@ -1,5 +1,5 @@ import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core'; -import { RbfTree } from '../../interfaces/node-api.interface'; +import { RbfTree } from '@interfaces/node-api.interface'; @Component({ selector: 'app-rbf-timeline-tooltip', diff --git a/frontend/src/app/components/rbf-timeline/rbf-timeline.component.ts b/frontend/src/app/components/rbf-timeline/rbf-timeline.component.ts index 83654a137..8bf5a0694 100644 --- a/frontend/src/app/components/rbf-timeline/rbf-timeline.component.ts +++ b/frontend/src/app/components/rbf-timeline/rbf-timeline.component.ts @@ -1,8 +1,8 @@ import { Component, Input, OnInit, OnChanges, Inject, LOCALE_ID, HostListener } from '@angular/core'; import { Router } from '@angular/router'; -import { RbfTree, RbfTransaction } from '../../interfaces/node-api.interface'; -import { StateService } from '../../services/state.service'; -import { ApiService } from '../../services/api.service'; +import { RbfTree, RbfTransaction } from '@interfaces/node-api.interface'; +import { StateService } from '@app/services/state.service'; +import { ApiService } from '@app/services/api.service'; type Connector = 'pipe' | 'corner'; diff --git a/frontend/src/app/components/reward-stats/reward-stats.component.ts b/frontend/src/app/components/reward-stats/reward-stats.component.ts index 5aac641b0..34dc55222 100644 --- a/frontend/src/app/components/reward-stats/reward-stats.component.ts +++ b/frontend/src/app/components/reward-stats/reward-stats.component.ts @@ -1,8 +1,8 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { concat, Observable } from 'rxjs'; import { map, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from '../../services/api.service'; -import { StateService } from '../../services/state.service'; +import { ApiService } from '@app/services/api.service'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-reward-stats', diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts index 3f48861d5..c0654c372 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -1,15 +1,15 @@ import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef, Input } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { EventType, NavigationStart, Router } from '@angular/router'; -import { AssetsService } from '../../services/assets.service'; -import { Env, StateService } from '../../services/state.service'; +import { AssetsService } from '@app/services/assets.service'; +import { Env, StateService } from '@app/services/state.service'; import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs'; import { debounceTime, distinctUntilChanged, switchMap, catchError, map, startWith, tap } from 'rxjs/operators'; -import { ElectrsApiService } from '../../services/electrs-api.service'; -import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; -import { ApiService } from '../../services/api.service'; -import { SearchResultsComponent } from './search-results/search-results.component'; -import { Network, findOtherNetworks, getRegex, getTargetUrl, needBaseModuleChange } from '../../shared/regex.utils'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; +import { ApiService } from '@app/services/api.service'; +import { SearchResultsComponent } from '@components/search-form/search-results/search-results.component'; +import { Network, findOtherNetworks, getRegex, getTargetUrl, needBaseModuleChange } from '@app/shared/regex.utils'; @Component({ selector: 'app-search-form', diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.ts b/frontend/src/app/components/search-form/search-results/search-results.component.ts index 04976028b..6a4efcd87 100644 --- a/frontend/src/app/components/search-form/search-results/search-results.component.ts +++ b/frontend/src/app/components/search-form/search-results/search-results.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; -import { StateService } from '../../../services/state.service'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-search-results', diff --git a/frontend/src/app/components/server-health/server-health.component.ts b/frontend/src/app/components/server-health/server-health.component.ts index d10f3f0b6..6f92c0c93 100644 --- a/frontend/src/app/components/server-health/server-health.component.ts +++ b/frontend/src/app/components/server-health/server-health.component.ts @@ -1,8 +1,8 @@ import { Component, OnInit, ChangeDetectionStrategy, SecurityContext, ChangeDetectorRef } from '@angular/core'; -import { WebsocketService } from '../../services/websocket.service'; +import { WebsocketService } from '@app/services/websocket.service'; import { Observable, Subject, map, tap } from 'rxjs'; -import { StateService } from '../../services/state.service'; -import { HealthCheckHost } from '../../interfaces/websocket.interface'; +import { StateService } from '@app/services/state.service'; +import { HealthCheckHost } from '@interfaces/websocket.interface'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ diff --git a/frontend/src/app/components/server-health/server-status.component.ts b/frontend/src/app/components/server-health/server-status.component.ts index e1300a68d..7941d326d 100644 --- a/frontend/src/app/components/server-health/server-status.component.ts +++ b/frontend/src/app/components/server-health/server-status.component.ts @@ -1,8 +1,8 @@ import { Component, OnInit, ChangeDetectionStrategy, SecurityContext, OnDestroy, ChangeDetectorRef } from '@angular/core'; -import { WebsocketService } from '../../services/websocket.service'; +import { WebsocketService } from '@app/services/websocket.service'; import { Observable, Subject, Subscription, map, tap } from 'rxjs'; -import { StateService } from '../../services/state.service'; -import { HealthCheckHost } from '../../interfaces/websocket.interface'; +import { StateService } from '@app/services/state.service'; +import { HealthCheckHost } from '@interfaces/websocket.interface'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ diff --git a/frontend/src/app/components/start/start.component.ts b/frontend/src/app/components/start/start.component.ts index 78c31cde5..31317cab5 100644 --- a/frontend/src/app/components/start/start.component.ts +++ b/frontend/src/app/components/start/start.component.ts @@ -1,8 +1,10 @@ import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Input, ChangeDetectorRef, ChangeDetectionStrategy, AfterViewChecked } from '@angular/core'; import { Subscription } from 'rxjs'; -import { MarkBlockState, StateService } from '../../services/state.service'; -import { specialBlocks } from '../../app.constants'; -import { BlockExtended } from '../../interfaces/node-api.interface'; +import { MarkBlockState, StateService } from '@app/services/state.service'; +import { specialBlocks } from '@app/app.constants'; +import { BlockExtended } from '@interfaces/node-api.interface'; +import { Router, ActivatedRoute } from '@angular/router'; +import { handleDemoRedirect } from '../../shared/common.utils'; @Component({ selector: 'app-start', @@ -61,6 +63,8 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy { constructor( public stateService: StateService, private cd: ChangeDetectorRef, + private router: Router, + private route: ActivatedRoute ) { this.isiOS = ['iPhone','iPod','iPad'].includes((navigator as any)?.userAgentData?.platform || navigator.platform); if (this.stateService.network === '') { @@ -69,6 +73,8 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy { } ngOnInit() { + handleDemoRedirect(this.route, this.router); + this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount); this.blockCounterSubscription = this.stateService.blocks$.subscribe((blocks) => { this.blockCount = blocks.length; diff --git a/frontend/src/app/components/statistics/statistics.component.ts b/frontend/src/app/components/statistics/statistics.component.ts index 835b74227..9dda3c496 100644 --- a/frontend/src/app/components/statistics/statistics.component.ts +++ b/frontend/src/app/components/statistics/statistics.component.ts @@ -4,16 +4,16 @@ import { UntypedFormGroup, UntypedFormBuilder } from '@angular/forms'; import { of, merge} from 'rxjs'; import { switchMap } from 'rxjs/operators'; -import { OptimizedMempoolStats } from '../../interfaces/node-api.interface'; -import { WebsocketService } from '../../services/websocket.service'; -import { ApiService } from '../../services/api.service'; +import { OptimizedMempoolStats } from '@interfaces/node-api.interface'; +import { WebsocketService } from '@app/services/websocket.service'; +import { ApiService } from '@app/services/api.service'; -import { StateService } from '../../services/state.service'; -import { SeoService } from '../../services/seo.service'; -import { StorageService } from '../../services/storage.service'; -import { feeLevels, chartColors } from '../../app.constants'; -import { MempoolGraphComponent } from '../mempool-graph/mempool-graph.component'; -import { IncomingTransactionsGraphComponent } from '../incoming-transactions-graph/incoming-transactions-graph.component'; +import { StateService } from '@app/services/state.service'; +import { SeoService } from '@app/services/seo.service'; +import { StorageService } from '@app/services/storage.service'; +import { feeLevels, chartColors } from '@app/app.constants'; +import { MempoolGraphComponent } from '@components/mempool-graph/mempool-graph.component'; +import { IncomingTransactionsGraphComponent } from '@components/incoming-transactions-graph/incoming-transactions-graph.component'; @Component({ selector: 'app-statistics', diff --git a/frontend/src/app/components/status-view/status-view.component.ts b/frontend/src/app/components/status-view/status-view.component.ts index 46e2347c7..4a9a75fec 100644 --- a/frontend/src/app/components/status-view/status-view.component.ts +++ b/frontend/src/app/components/status-view/status-view.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { WebsocketService } from '../../services/websocket.service'; +import { WebsocketService } from '@app/services/websocket.service'; @Component({ selector: 'app-status-view', diff --git a/frontend/src/app/components/svg-images/svg-images.component.html b/frontend/src/app/components/svg-images/svg-images.component.html index 48e13de67..34ed23bd0 100644 --- a/frontend/src/app/components/svg-images/svg-images.component.html +++ b/frontend/src/app/components/svg-images/svg-images.component.html @@ -93,7 +93,7 @@ - + Mempool Accelerator™ @@ -113,6 +113,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/app/components/television/television.component.ts b/frontend/src/app/components/television/television.component.ts index 40f4b7192..1507f3d97 100644 --- a/frontend/src/app/components/television/television.component.ts +++ b/frontend/src/app/components/television/television.component.ts @@ -1,9 +1,9 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; -import { WebsocketService } from '../../services/websocket.service'; -import { OptimizedMempoolStats } from '../../interfaces/node-api.interface'; -import { StateService } from '../../services/state.service'; -import { ApiService } from '../../services/api.service'; -import { SeoService } from '../../services/seo.service'; +import { WebsocketService } from '@app/services/websocket.service'; +import { OptimizedMempoolStats } from '@interfaces/node-api.interface'; +import { StateService } from '@app/services/state.service'; +import { ApiService } from '@app/services/api.service'; +import { SeoService } from '@app/services/seo.service'; import { ActivatedRoute } from '@angular/router'; import { map, scan, startWith, switchMap, tap } from 'rxjs/operators'; import { interval, merge, Observable, Subscription } from 'rxjs'; diff --git a/frontend/src/app/components/terms-of-service/terms-of-service.component.ts b/frontend/src/app/components/terms-of-service/terms-of-service.component.ts index 71a86c759..5eb90c0d9 100644 --- a/frontend/src/app/components/terms-of-service/terms-of-service.component.ts +++ b/frontend/src/app/components/terms-of-service/terms-of-service.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; -import { Env, StateService } from '../../services/state.service'; -import { SeoService } from '../../services/seo.service'; -import { OpenGraphService } from '../../services/opengraph.service'; +import { Env, StateService } from '@app/services/state.service'; +import { SeoService } from '@app/services/seo.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; @Component({ selector: 'app-terms-of-service', diff --git a/frontend/src/app/components/terms-of-service/terms-of-service.module.ts b/frontend/src/app/components/terms-of-service/terms-of-service.module.ts index 2ab139d8b..8a758b8de 100644 --- a/frontend/src/app/components/terms-of-service/terms-of-service.module.ts +++ b/frontend/src/app/components/terms-of-service/terms-of-service.module.ts @@ -1,8 +1,8 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Routes, RouterModule } from '@angular/router'; -import { TermsOfServiceComponent } from './terms-of-service.component'; -import { SharedModule } from '../../shared/shared.module'; +import { TermsOfServiceComponent } from '@components/terms-of-service/terms-of-service.component'; +import { SharedModule } from '@app/shared/shared.module'; const routes: Routes = [ { diff --git a/frontend/src/app/components/test-transactions/test-transactions.component.html b/frontend/src/app/components/test-transactions/test-transactions.component.html index 20ba5c4bd..181db8b01 100644 --- a/frontend/src/app/components/test-transactions/test-transactions.component.html +++ b/frontend/src/app/components/test-transactions/test-transactions.component.html @@ -1,5 +1,8 @@
-

Test Transactions

+
+

Test Transactions

+ +
diff --git a/frontend/src/app/components/test-transactions/test-transactions.component.ts b/frontend/src/app/components/test-transactions/test-transactions.component.ts index c9abeed62..22a0951ea 100644 --- a/frontend/src/app/components/test-transactions/test-transactions.component.ts +++ b/frontend/src/app/components/test-transactions/test-transactions.component.ts @@ -1,10 +1,10 @@ import { Component, OnInit } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; -import { ApiService } from '../../services/api.service'; -import { StateService } from '../../services/state.service'; -import { SeoService } from '../../services/seo.service'; -import { OpenGraphService } from '../../services/opengraph.service'; -import { TestMempoolAcceptResult } from '../../interfaces/node-api.interface'; +import { ApiService } from '@app/services/api.service'; +import { StateService } from '@app/services/state.service'; +import { SeoService } from '@app/services/seo.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; +import { TestMempoolAcceptResult } from '@interfaces/node-api.interface'; @Component({ selector: 'app-test-transactions', @@ -74,7 +74,7 @@ export class TestTransactionsComponent implements OnInit { }, (error) => { if (typeof error.error === 'string') { - const matchText = error.error.match('"message":"(.*?)"'); + const matchText = error.error.replace(/\\/g, '').match('"message":"(.*?)"'); this.error = matchText && matchText[1] || error.error; } else if (error.message) { this.error = error.message; diff --git a/frontend/src/app/components/theme-selector/theme-selector.component.ts b/frontend/src/app/components/theme-selector/theme-selector.component.ts index be207910c..ca9c5788d 100644 --- a/frontend/src/app/components/theme-selector/theme-selector.component.ts +++ b/frontend/src/app/components/theme-selector/theme-selector.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { ThemeService } from '../../services/theme.service'; +import { ThemeService } from '@app/services/theme.service'; import { Subscription } from 'rxjs'; @Component({ diff --git a/frontend/src/app/components/time/time.component.ts b/frontend/src/app/components/time/time.component.ts index 3015007b2..9ae893d74 100644 --- a/frontend/src/app/components/time/time.component.ts +++ b/frontend/src/app/components/time/time.component.ts @@ -1,7 +1,6 @@ import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core'; -import { StateService } from '../../services/state.service'; -import { dates } from '../../shared/i18n/dates'; -import { DatePipe } from '@angular/common'; +import { StateService } from '@app/services/state.service'; +import { TimeService } from '@app/services/time.service'; @Component({ selector: 'app-time', @@ -12,19 +11,9 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { interval: number; text: string; tooltip: string; - precisionThresholds = { - year: 100, - month: 18, - week: 12, - day: 31, - hour: 48, - minute: 90, - second: 90 - }; - intervals = {}; @Input() time: number; - @Input() dateString: number; + @Input() dateString: string; @Input() kind: 'plain' | 'since' | 'until' | 'span' | 'before' | 'within' = 'plain'; @Input() fastRender = false; @Input() fixedRender = false; @@ -40,37 +29,26 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { constructor( private ref: ChangeDetectorRef, private stateService: StateService, - private datePipe: DatePipe, - ) { - this.intervals = { - year: 31536000, - month: 2592000, - week: 604800, - day: 86400, - hour: 3600, - minute: 60, - second: 1 - }; - } + private timeService: TimeService, + ) {} ngOnInit() { + this.calculateTime(); if(this.fixedRender){ - this.text = this.calculate(); return; } if (!this.stateService.isBrowser) { - this.text = this.calculate(); this.ref.markForCheck(); return; } this.interval = window.setInterval(() => { - this.text = this.calculate(); + this.calculateTime(); this.ref.markForCheck(); }, 1000 * (this.fastRender ? 1 : 60)); } ngOnChanges() { - this.text = this.calculate(); + this.calculateTime(); this.ref.markForCheck(); } @@ -78,224 +56,21 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { clearInterval(this.interval); } - calculate() { - if (this.time == null) { - return; - } - - let seconds: number; - switch (this.kind) { - case 'since': - seconds = Math.floor((+new Date() - +new Date(this.dateString || this.time * 1000)) / 1000); - this.tooltip = this.datePipe.transform(new Date(this.dateString || this.time * 1000), 'yyyy-MM-dd HH:mm'); - break; - case 'until': - case 'within': - seconds = (+new Date(this.time) - +new Date()) / 1000; - this.tooltip = this.datePipe.transform(new Date(this.time), 'yyyy-MM-dd HH:mm'); - break; - default: - seconds = Math.floor(this.time); - this.tooltip = ''; - } - - if (!this.showTooltip || this.relative) { - this.tooltip = ''; - } - - if (seconds < 1 && this.kind === 'span') { - return $localize`:@@date-base.immediately:Immediately`; - } else if (seconds < 60) { - if (this.relative || this.kind === 'since') { - if (this.lowercaseStart) { - return $localize`:@@date-base.just-now:Just now`.charAt(0).toLowerCase() + $localize`:@@date-base.just-now:Just now`.slice(1); - } - return $localize`:@@date-base.just-now:Just now`; - } else if (this.kind === 'until' || this.kind === 'within') { - seconds = 60; - } - } - - let counter: number; - const result = []; - let usedUnits = 0; - for (const [index, unit] of this.units.entries()) { - let precisionUnit = this.units[Math.min(this.units.length - 1, index + this.precision)]; - counter = Math.floor(seconds / this.intervals[unit]); - const precisionCounter = Math.round(seconds / this.intervals[precisionUnit]); - if (precisionCounter > this.precisionThresholds[precisionUnit]) { - precisionUnit = unit; - } - if (this.units.indexOf(precisionUnit) === this.units.indexOf(this.minUnit)) { - counter = Math.max(1, counter); - } - if (counter > 0) { - let rounded; - const roundFactor = Math.pow(10,this.fractionDigits || 0); - if ((this.kind === 'until' || this.kind === 'within') && usedUnits < this.numUnits) { - rounded = Math.floor((seconds / this.intervals[precisionUnit]) * roundFactor) / roundFactor; - } else { - rounded = Math.round((seconds / this.intervals[precisionUnit]) * roundFactor) / roundFactor; - } - if ((this.kind !== 'until' && this.kind !== 'within')|| this.numUnits === 1) { - return this.formatTime(this.kind, precisionUnit, rounded); - } else { - if (!usedUnits) { - result.push(this.formatTime(this.kind, precisionUnit, rounded)); - } else { - result.push(this.formatTime('', precisionUnit, rounded)); - } - seconds -= (rounded * this.intervals[precisionUnit]); - usedUnits++; - if (usedUnits >= this.numUnits) { - return result.join(', '); - } - } - } - } - return result.join(', '); - } - - private formatTime(kind, unit, number): string { - const dateStrings = dates(number); - switch (kind) { - case 'since': - if (number === 1) { - switch (unit) { // singular (1 day) - case 'year': return $localize`:@@time-since:${dateStrings.i18nYear}:DATE: ago`; break; - case 'month': return $localize`:@@time-since:${dateStrings.i18nMonth}:DATE: ago`; break; - case 'week': return $localize`:@@time-since:${dateStrings.i18nWeek}:DATE: ago`; break; - case 'day': return $localize`:@@time-since:${dateStrings.i18nDay}:DATE: ago`; break; - case 'hour': return $localize`:@@time-since:${dateStrings.i18nHour}:DATE: ago`; break; - case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinute}:DATE: ago`; break; - case 'second': return $localize`:@@time-since:${dateStrings.i18nSecond}:DATE: ago`; break; - } - } else { - switch (unit) { // plural (2 days) - case 'year': return $localize`:@@time-since:${dateStrings.i18nYears}:DATE: ago`; break; - case 'month': return $localize`:@@time-since:${dateStrings.i18nMonths}:DATE: ago`; break; - case 'week': return $localize`:@@time-since:${dateStrings.i18nWeeks}:DATE: ago`; break; - case 'day': return $localize`:@@time-since:${dateStrings.i18nDays}:DATE: ago`; break; - case 'hour': return $localize`:@@time-since:${dateStrings.i18nHours}:DATE: ago`; break; - case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinutes}:DATE: ago`; break; - case 'second': return $localize`:@@time-since:${dateStrings.i18nSeconds}:DATE: ago`; break; - } - } - break; - case 'until': - if (number === 1) { - switch (unit) { // singular (In ~1 day) - case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYear}:DATE:`; break; - case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonth}:DATE:`; break; - case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeek}:DATE:`; break; - case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDay}:DATE:`; break; - case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHour}:DATE:`; break; - case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinute}:DATE:`; - case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSecond}:DATE:`; - } - } else { - switch (unit) { // plural (In ~2 days) - case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYears}:DATE:`; break; - case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonths}:DATE:`; break; - case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeeks}:DATE:`; break; - case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDays}:DATE:`; break; - case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHours}:DATE:`; break; - case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinutes}:DATE:`; break; - case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSeconds}:DATE:`; break; - } - } - break; - case 'within': - if (number === 1) { - switch (unit) { // singular (In ~1 day) - case 'year': return $localize`:@@time-within:within ~${dateStrings.i18nYear}:DATE:`; break; - case 'month': return $localize`:@@time-within:within ~${dateStrings.i18nMonth}:DATE:`; break; - case 'week': return $localize`:@@time-within:within ~${dateStrings.i18nWeek}:DATE:`; break; - case 'day': return $localize`:@@time-within:within ~${dateStrings.i18nDay}:DATE:`; break; - case 'hour': return $localize`:@@time-within:within ~${dateStrings.i18nHour}:DATE:`; break; - case 'minute': return $localize`:@@time-within:within ~${dateStrings.i18nMinute}:DATE:`; - case 'second': return $localize`:@@time-within:within ~${dateStrings.i18nSecond}:DATE:`; - } - } else { - switch (unit) { // plural (In ~2 days) - case 'year': return $localize`:@@time-within:within ~${dateStrings.i18nYears}:DATE:`; break; - case 'month': return $localize`:@@time-within:within ~${dateStrings.i18nMonths}:DATE:`; break; - case 'week': return $localize`:@@time-within:within ~${dateStrings.i18nWeeks}:DATE:`; break; - case 'day': return $localize`:@@time-within:within ~${dateStrings.i18nDays}:DATE:`; break; - case 'hour': return $localize`:@@time-within:within ~${dateStrings.i18nHours}:DATE:`; break; - case 'minute': return $localize`:@@time-within:within ~${dateStrings.i18nMinutes}:DATE:`; break; - case 'second': return $localize`:@@time-within:within ~${dateStrings.i18nSeconds}:DATE:`; break; - } - } - break; - case 'span': - if (number === 1) { - switch (unit) { // singular (1 day) - case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYear}:DATE:`; break; - case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonth}:DATE:`; break; - case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeek}:DATE:`; break; - case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDay}:DATE:`; break; - case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHour}:DATE:`; break; - case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinute}:DATE:`; break; - case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSecond}:DATE:`; break; - } - } else { - switch (unit) { // plural (2 days) - case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYears}:DATE:`; break; - case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonths}:DATE:`; break; - case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeeks}:DATE:`; break; - case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDays}:DATE:`; break; - case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHours}:DATE:`; break; - case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinutes}:DATE:`; break; - case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSeconds}:DATE:`; break; - } - } - break; - case 'before': - if (number === 1) { - switch (unit) { // singular (1 day) - case 'year': return $localize`:@@time-before:${dateStrings.i18nYear}:DATE: before`; break; - case 'month': return $localize`:@@time-before:${dateStrings.i18nMonth}:DATE: before`; break; - case 'week': return $localize`:@@time-before:${dateStrings.i18nWeek}:DATE: before`; break; - case 'day': return $localize`:@@time-before:${dateStrings.i18nDay}:DATE: before`; break; - case 'hour': return $localize`:@@time-before:${dateStrings.i18nHour}:DATE: before`; break; - case 'minute': return $localize`:@@time-before:${dateStrings.i18nMinute}:DATE: before`; break; - case 'second': return $localize`:@@time-before:${dateStrings.i18nSecond}:DATE: before`; break; - } - } else { - switch (unit) { // plural (2 days) - case 'year': return $localize`:@@time-before:${dateStrings.i18nYears}:DATE: before`; break; - case 'month': return $localize`:@@time-before:${dateStrings.i18nMonths}:DATE: before`; break; - case 'week': return $localize`:@@time-before:${dateStrings.i18nWeeks}:DATE: before`; break; - case 'day': return $localize`:@@time-before:${dateStrings.i18nDays}:DATE: before`; break; - case 'hour': return $localize`:@@time-before:${dateStrings.i18nHours}:DATE: before`; break; - case 'minute': return $localize`:@@time-before:${dateStrings.i18nMinutes}:DATE: before`; break; - case 'second': return $localize`:@@time-before:${dateStrings.i18nSeconds}:DATE: before`; break; - } - } - break; - default: - if (number === 1) { - switch (unit) { // singular (1 day) - case 'year': return dateStrings.i18nYear; break; - case 'month': return dateStrings.i18nMonth; break; - case 'week': return dateStrings.i18nWeek; break; - case 'day': return dateStrings.i18nDay; break; - case 'hour': return dateStrings.i18nHour; break; - case 'minute': return dateStrings.i18nMinute; break; - case 'second': return dateStrings.i18nSecond; break; - } - } else { - switch (unit) { // plural (2 days) - case 'year': return dateStrings.i18nYears; break; - case 'month': return dateStrings.i18nMonths; break; - case 'week': return dateStrings.i18nWeeks; break; - case 'day': return dateStrings.i18nDays; break; - case 'hour': return dateStrings.i18nHours; break; - case 'minute': return dateStrings.i18nMinutes; break; - case 'second': return dateStrings.i18nSeconds; break; - } - } - } + calculateTime(): void { + const { text, tooltip } = this.timeService.calculate( + this.time, + this.kind, + this.relative, + this.precision, + this.minUnit, + this.showTooltip, + this.units, + this.dateString, + this.lowercaseStart, + this.numUnits, + this.fractionDigits, + ); + this.text = text; + this.tooltip = tooltip; } } diff --git a/frontend/src/app/components/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html index d467aae80..2d9bd4982 100644 --- a/frontend/src/app/components/tracker/tracker.component.html +++ b/frontend/src/app/components/tracker/tracker.component.html @@ -4,7 +4,7 @@ \ No newline at end of file diff --git a/frontend/src/app/components/transaction/transaction.component.scss b/frontend/src/app/components/transaction/transaction.component.scss index 232a2cacb..7125a6b46 100644 --- a/frontend/src/app/components/transaction/transaction.component.scss +++ b/frontend/src/app/components/transaction/transaction.component.scss @@ -18,6 +18,7 @@ line-height: 1; } } + .tx-link { display: flex; flex-direction: row; @@ -81,7 +82,7 @@ margin-bottom: 40px; } -.row{ +.row { flex-direction: column; @media (min-width: 850px) { flex-direction: row; @@ -287,37 +288,21 @@ } .accelerate { - display: flex !important; - align-self: auto; - margin-left: auto; - background-color: var(--tertiary); - @media (max-width: 849px) { - margin-left: 5px; - } + @media (min-width: 850px) { + margin-left: auto; + } } .etaDeepMempool { - justify-content: flex-end; flex-wrap: wrap; - align-content: center; - @media (max-width: 995px) { - justify-content: left !important; - } @media (max-width: 849px) { justify-content: right !important; } } .accelerateDeepMempool { - align-self: auto; - margin-left: auto; background-color: var(--tertiary); - @media (max-width: 995px) { - margin-left: 0px; - } - @media (max-width: 849px) { - margin-left: 5px; - } + margin-left: 5px; } .goggles-icon { @@ -335,4 +320,9 @@ .oobFees { color: #905cf4; -} \ No newline at end of file +} + +.disabled { + opacity: 0.5; + pointer-events: none; +} diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index c80006552..5f5ef4fa7 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, AfterViewInit, OnDestroy, HostListener, ViewChild, ElementRef, Inject, ChangeDetectorRef } from '@angular/core'; -import { ElectrsApiService } from '../../services/electrs-api.service'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { switchMap, @@ -15,33 +15,34 @@ import { repeat, take } from 'rxjs/operators'; -import { Transaction } from '../../interfaces/electrs.interface'; +import { Transaction } from '@interfaces/electrs.interface'; import { of, merge, Subscription, Observable, Subject, from, throwError, combineLatest, BehaviorSubject } from 'rxjs'; -import { StateService } from '../../services/state.service'; -import { CacheService } from '../../services/cache.service'; -import { WebsocketService } from '../../services/websocket.service'; -import { AudioService } from '../../services/audio.service'; -import { ApiService } from '../../services/api.service'; -import { SeoService } from '../../services/seo.service'; -import { StorageService } from '../../services/storage.service'; -import { seoDescriptionNetwork } from '../../shared/common.utils'; -import { getTransactionFlags, getUnacceleratedFeeRate } from '../../shared/transaction.utils'; -import { Filter, TransactionFlags, toFilters } from '../../shared/filters.utils'; -import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration, AccelerationPosition } from '../../interfaces/node-api.interface'; -import { LiquidUnblinding } from './liquid-ublinding'; -import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; -import { PriceService } from '../../services/price.service'; -import { isFeatureActive } from '../../bitcoin.utils'; -import { ServicesApiServices } from '../../services/services-api.service'; -import { EnterpriseService } from '../../services/enterprise.service'; -import { ZONE_SERVICE } from '../../injection-tokens'; -import { MiningService, MiningStats } from '../../services/mining.service'; -import { ETA, EtaService } from '../../services/eta.service'; +import { StateService } from '@app/services/state.service'; +import { CacheService } from '@app/services/cache.service'; +import { WebsocketService } from '@app/services/websocket.service'; +import { AudioService } from '@app/services/audio.service'; +import { ApiService } from '@app/services/api.service'; +import { SeoService } from '@app/services/seo.service'; +import { StorageService } from '@app/services/storage.service'; +import { seoDescriptionNetwork } from '@app/shared/common.utils'; +import { getTransactionFlags, getUnacceleratedFeeRate } from '@app/shared/transaction.utils'; +import { Filter, TransactionFlags, toFilters } from '@app/shared/filters.utils'; +import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration, AccelerationPosition } from '@interfaces/node-api.interface'; +import { LiquidUnblinding } from '@components/transaction/liquid-ublinding'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; +import { PriceService } from '@app/services/price.service'; +import { isFeatureActive } from '@app/bitcoin.utils'; +import { ServicesApiServices } from '@app/services/services-api.service'; +import { EnterpriseService } from '@app/services/enterprise.service'; +import { ZONE_SERVICE } from '@app/injection-tokens'; +import { MiningService, MiningStats } from '@app/services/mining.service'; +import { ETA, EtaService } from '@app/services/eta.service'; -interface Pool { +export interface Pool { id: number; name: string; slug: string; + minerNames: string[] | null; } export interface TxAuditStatus { @@ -118,7 +119,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { txChanged$ = new BehaviorSubject(false); // triggered whenever this.tx changes (long term, we should refactor to make this.tx an observable itself) isAccelerated$ = new BehaviorSubject(false); // refactor this to make isAccelerated an observable itself ETA$: Observable; - standardETA$: Observable; isCached: boolean = false; now = Date.now(); da$: Observable; @@ -139,6 +139,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { firstLoad = true; waitingForAccelerationInfo: boolean = false; isLoadingFirstSeen = false; + notAcceleratedOnLoad: boolean = null; featuresEnabled: boolean; segwitEnabled: boolean; @@ -191,7 +192,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.hideAccelerationSummary = this.stateService.isMempoolSpaceBuild ? this.storageService.getValue('hide-accelerator-pref') == 'true' : true; if (!this.stateService.isLiquid()) { - this.miningService.getMiningStats('1w').subscribe(stats => { + this.miningService.getMiningStats('1m').subscribe(stats => { this.miningStats = stats; }); } @@ -343,7 +344,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.setIsAccelerated(); }), switchMap((blockHeight: number) => { - return this.servicesApiService.getAccelerationHistory$({ blockHeight }).pipe( + return this.servicesApiService.getAllAccelerationHistory$({ blockHeight }, null, this.txId).pipe( switchMap((accelerationHistory: Acceleration[]) => { if (this.tx.acceleration && !accelerationHistory.length) { // If the just mined transaction was accelerated, but services backend did not return any acceleration data, retry return throwError('retry'); @@ -490,7 +491,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { if (this.stateService.network === '') { if (!this.mempoolPosition.accelerated) { if (!this.accelerationFlowCompleted && !this.hideAccelerationSummary && !this.showAccelerationSummary) { - this.miningService.getMiningStats('1w').subscribe(stats => { + this.miningService.getMiningStats('1m').subscribe(stats => { this.miningStats = stats; }); } @@ -848,6 +849,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.tx.feeDelta = cpfpInfo.feeDelta; this.setIsAccelerated(firstCpfp); } + + if (this.notAcceleratedOnLoad === null) { + this.notAcceleratedOnLoad = !this.isAcceleration; + } if (!this.isAcceleration && this.fragmentParams.has('accelerate')) { this.forceAccelerationSummary = true; @@ -877,21 +882,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.miningStats = stats; this.isAccelerated$.next(this.isAcceleration); // hack to trigger recalculation of ETA without adding another source observable }); - if (!this.tx.status?.confirmed) { - this.standardETA$ = combineLatest([ - this.stateService.mempoolBlocks$.pipe(startWith(null)), - this.stateService.difficultyAdjustment$.pipe(startWith(null)), - ]).pipe( - map(([mempoolBlocks, da]) => { - return this.etaService.calculateUnacceleratedETA( - this.tx, - mempoolBlocks, - da, - this.cpfpInfo, - ); - }) - ) - } } this.isAccelerated$.next(this.isAcceleration); } @@ -966,6 +956,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.filters = []; this.showCpfpDetails = false; this.showAccelerationDetails = false; + this.accelerationFlowCompleted = false; this.accelerationInfo = null; this.cashappEligible = false; this.txInBlockIndex = null; @@ -1083,6 +1074,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { (!this.hideAccelerationSummary && !this.accelerationFlowCompleted) || this.forceAccelerationSummary ) + && this.notAcceleratedOnLoad // avoid briefly showing accelerator checkout on already accelerated txs ); } diff --git a/frontend/src/app/components/transaction/transaction.module.ts b/frontend/src/app/components/transaction/transaction.module.ts index b98c33e2a..80de0cf40 100644 --- a/frontend/src/app/components/transaction/transaction.module.ts +++ b/frontend/src/app/components/transaction/transaction.module.ts @@ -1,12 +1,14 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Routes, RouterModule } from '@angular/router'; -import { TransactionComponent } from './transaction.component'; -import { SharedModule } from '../../shared/shared.module'; -import { TxBowtieModule } from '../tx-bowtie-graph/tx-bowtie.module'; -import { GraphsModule } from '../../graphs/graphs.module'; -import { AccelerateCheckout } from '../accelerate-checkout/accelerate-checkout.component'; -import { AccelerateFeeGraphComponent } from '../accelerate-checkout/accelerate-fee-graph.component'; +import { TransactionComponent } from '@components/transaction/transaction.component'; +import { TransactionDetailsComponent } from '@components/transaction/transaction-details/transaction-details.component'; +import { SharedModule } from '@app/shared/shared.module'; +import { TxBowtieModule } from '@components/tx-bowtie-graph/tx-bowtie.module'; +import { TransactionExtrasModule } from '@components/transaction/transaction-extras.module'; +import { GraphsModule } from '@app/graphs/graphs.module'; +import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component'; +import { AccelerateFeeGraphComponent } from '@components/accelerate-checkout/accelerate-fee-graph.component'; const routes: Routes = [ { @@ -40,14 +42,17 @@ export class TransactionRoutingModule { } SharedModule, GraphsModule, TxBowtieModule, + TransactionExtrasModule, ], declarations: [ TransactionComponent, + TransactionDetailsComponent, AccelerateCheckout, AccelerateFeeGraphComponent, ], exports: [ TransactionComponent, + TransactionDetailsComponent, AccelerateCheckout, AccelerateFeeGraphComponent, ] diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html index 8954e4ecb..5ad1c798c 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -23,7 +23,7 @@ @@ -81,7 +81,8 @@
- + +
@@ -96,6 +97,15 @@ + + + + + + @@ -204,7 +214,7 @@ + + + +
@@ -236,7 +246,12 @@ - OP_RETURN {{ vout.scriptpubkey_asm | hex2ascii }} + OP_RETURN  + @if (vout.isRunestone) { + + } @else { + {{ vout.scriptpubkey_asm | hex2ascii }} + } {{ vout.scriptpubkey_type | scriptpubkeyType }} @@ -276,6 +291,15 @@
+ +
@@ -321,7 +345,7 @@
 – {{ tx.fee | number }} sat + i18n="shared.sats">sats
Show more inputs to reveal fee data
@@ -329,7 +353,7 @@ -
+ + + + + + + + + + + + + + + + + + + + +
Addresses + + + + +
+
+ +
+ +
+
Confirmed balance
Confirmed UTXOs{{ walletStats.utxos }}
Total received
+ +
+
+ +
+ + + + +
+
+

Balance History

+
+
+
+
+ +
+
+
+
+ + + + +
+
+
+ + + + + + + + + + + + +
+
+
+
+ +
+
+
+ +
+ + +
+ +
+ Error loading wallet data. +

+ ({{ error | httpErrorMsg }}) +
+
+ + + Error loading wallet data. + + +
+ + + +
+ + +
+ +
+
diff --git a/frontend/src/app/components/wallet/wallet.component.scss b/frontend/src/app/components/wallet/wallet.component.scss new file mode 100644 index 000000000..6723cffbc --- /dev/null +++ b/frontend/src/app/components/wallet/wallet.component.scss @@ -0,0 +1,117 @@ +.qr-wrapper { + background-color: #FFF; + padding: 10px; + padding-bottom: 5px; + display: inline-block; +} + +.treemap-col { + width: 45%; + height: 300px; +} + +.fiat { + display: block; + @media (min-width: 992px){ + display: inline-block; + margin-left: 10px; + } +} + +.table { + tr td { + &:last-child { + text-align: right; + @media (min-width: 576px) { + text-align: left; + } + } + } +} + +.address-list { + width: 100%; + max-width: 200px; +} + +h1 { + margin: 0px; + padding: 0px; + margin-right: 10px; + font-size: 1.9rem; + @media (min-width: 576px) { + font-size: 2rem; + float: left; + } + @media (min-width: 768px) { + font-size: 2.5rem; + } +} + +.title-address { + align-items: baseline; +} + +.address-link { + line-height: 56px; + margin-left: 0px; + top: -2px; + position: relative; + @media (min-width: 768px) { + line-height: 69px; + } +} + +.row{ + flex-direction: column; + @media (min-width: 576px) { + flex-direction: row; + } +} + +@media (max-width: 767.98px) { + .mobile-bottomcol { + margin-top: 15px; + } + .details-table td:first-child { + white-space: pre-wrap; + } +} + +.tx-link { + display: block; + height: 100%; + top: 9px; + position: relative; + @media (min-width: 576px) { + top: 11px; + } + @media (min-width: 768px) { + max-width: calc(100% - 180px); + top: 17px; + } +} + +.title-tx { + h2 { + line-height: 1; + margin-bottom: 10px; + } +} + +.liquid-address { + .address-table { + table-layout: fixed; + + tr td:first-child { + width: 170px; + } + tr td:last-child { + width: 80%; + } + } + + .qrcode-col { + flex-grow: 0.5; + } +} diff --git a/frontend/src/app/components/wallet/wallet.component.ts b/frontend/src/app/components/wallet/wallet.component.ts new file mode 100644 index 000000000..ce44250e9 --- /dev/null +++ b/frontend/src/app/components/wallet/wallet.component.ts @@ -0,0 +1,303 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute, ParamMap } from '@angular/router'; +import { switchMap, catchError, map, tap, shareReplay, startWith, scan } from 'rxjs/operators'; +import { Address, AddressTxSummary, ChainStats, Transaction } from '@interfaces/electrs.interface'; +import { WebsocketService } from '@app/services/websocket.service'; +import { StateService } from '@app/services/state.service'; +import { ApiService } from '@app/services/api.service'; +import { of, Observable, Subscription } from 'rxjs'; +import { SeoService } from '@app/services/seo.service'; +import { seoDescriptionNetwork } from '@app/shared/common.utils'; +import { WalletAddress } from '@interfaces/node-api.interface'; + +class WalletStats implements ChainStats { + addresses: string[]; + funded_txo_count: number; + funded_txo_sum: number; + spent_txo_count: number; + spent_txo_sum: number; + tx_count: number; + + constructor (stats: ChainStats[], addresses: string[]) { + Object.assign(this, stats.reduce((acc, stat) => { + acc.funded_txo_count += stat.funded_txo_count; + acc.funded_txo_sum += stat.funded_txo_sum; + acc.spent_txo_count += stat.spent_txo_count; + acc.spent_txo_sum += stat.spent_txo_sum; + return acc; + }, { + funded_txo_count: 0, + funded_txo_sum: 0, + spent_txo_count: 0, + spent_txo_sum: 0, + tx_count: 0, + }) + ); + this.addresses = addresses; + } + + public addTx(tx: Transaction): void { + for (const vin of tx.vin) { + if (this.addresses.includes(vin.prevout?.scriptpubkey_address)) { + this.spendTxo(vin.prevout.value); + } + } + for (const vout of tx.vout) { + if (this.addresses.includes(vout.scriptpubkey_address)) { + this.fundTxo(vout.value); + } + } + this.tx_count++; + } + + public removeTx(tx: Transaction): void { + for (const vin of tx.vin) { + if (this.addresses.includes(vin.prevout?.scriptpubkey_address)) { + this.unspendTxo(vin.prevout.value); + } + } + for (const vout of tx.vout) { + if (this.addresses.includes(vout.scriptpubkey_address)) { + this.unfundTxo(vout.value); + } + } + this.tx_count--; + } + + private fundTxo(value: number): void { + this.funded_txo_sum += value; + this.funded_txo_count++; + } + + private unfundTxo(value: number): void { + this.funded_txo_sum -= value; + this.funded_txo_count--; + } + + private spendTxo(value: number): void { + this.spent_txo_sum += value; + this.spent_txo_count++; + } + + private unspendTxo(value: number): void { + this.spent_txo_sum -= value; + this.spent_txo_count--; + } + + get balance(): number { + return this.funded_txo_sum - this.spent_txo_sum; + } + + get totalReceived(): number { + return this.funded_txo_sum; + } + + get utxos(): number { + return this.funded_txo_count - this.spent_txo_count; + } +} + +@Component({ + selector: 'app-wallet', + templateUrl: './wallet.component.html', + styleUrls: ['./wallet.component.scss'] +}) +export class WalletComponent implements OnInit, OnDestroy { + network = ''; + + addresses: Address[] = []; + addressStrings: string[] = []; + walletName: string; + isLoadingWallet = true; + wallet$: Observable>; + walletAddresses$: Observable>; + walletSummary$: Observable; + walletStats$: Observable; + error: any; + walletSubscription: Subscription; + + collapseAddresses: boolean = true; + + fullyLoaded = false; + txCount = 0; + received = 0; + sent = 0; + chainBalance = 0; + + constructor( + private route: ActivatedRoute, + private websocketService: WebsocketService, + private stateService: StateService, + private apiService: ApiService, + private seoService: SeoService, + ) { } + + ngOnInit(): void { + this.stateService.networkChanged$.subscribe((network) => this.network = network); + this.websocketService.want(['blocks']); + this.wallet$ = this.route.paramMap.pipe( + map((params: ParamMap) => params.get('wallet') as string), + tap((walletName: string) => { + this.walletName = walletName; + this.websocketService.startTrackingWallet(walletName); + this.seoService.setTitle($localize`:@@wallet.component.browser-title:Wallet: ${walletName}:INTERPOLATION:`); + this.seoService.setDescription($localize`:@@meta.description.bitcoin.wallet:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} wallet ${walletName}:INTERPOLATION:.`); + }), + switchMap((walletName: string) => this.apiService.getWallet$(walletName).pipe( + catchError((err) => { + this.error = err; + this.seoService.logSoft404(); + console.log(err); + return of({}); + }) + )), + shareReplay(1), + ); + + this.walletAddresses$ = this.wallet$.pipe( + map(wallet => { + const walletInfo: Record = {}; + for (const address of Object.keys(wallet)) { + walletInfo[address] = { + address, + chain_stats: wallet[address].stats, + mempool_stats: { + funded_txo_count: 0, + funded_txo_sum: 0, + spent_txo_count: 0, spent_txo_sum: 0, tx_count: 0 + }, + }; + } + return walletInfo; + }), + switchMap(initial => this.stateService.walletTransactions$.pipe( + startWith(null), + scan((wallet, walletTransactions) => { + for (const tx of (walletTransactions || [])) { + const funded: Record = {}; + const spent: Record = {}; + const fundedCount: Record = {}; + const spentCount: Record = {}; + for (const vin of tx.vin) { + const address = vin.prevout?.scriptpubkey_address; + if (address && wallet[address]) { + spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0); + spentCount[address] = (spentCount[address] ?? 0) + 1; + } + } + for (const vout of tx.vout) { + const address = vout.scriptpubkey_address; + if (address && wallet[address]) { + funded[address] = (funded[address] ?? 0) + (vout.value ?? 0); + fundedCount[address] = (fundedCount[address] ?? 0) + 1; + } + } + for (const address of Object.keys({ ...funded, ...spent })) { + // update address stats + wallet[address].chain_stats.tx_count++; + wallet[address].chain_stats.funded_txo_count += fundedCount[address] || 0; + wallet[address].chain_stats.spent_txo_count += spentCount[address] || 0; + wallet[address].chain_stats.funded_txo_sum += funded[address] || 0; + wallet[address].chain_stats.spent_txo_sum += spent[address] || 0; + } + } + return wallet; + }, initial) + )), + tap(() => { + this.isLoadingWallet = false; + }) + ); + + this.walletSubscription = this.walletAddresses$.subscribe(wallet => { + this.addressStrings = Object.keys(wallet); + this.addresses = Object.values(wallet); + }); + + this.walletSummary$ = this.wallet$.pipe( + switchMap(wallet => this.stateService.walletTransactions$.pipe( + startWith([]), + scan((summaries, newTransactions) => { + const newSummaries: AddressTxSummary[] = []; + for (const tx of newTransactions) { + const funded: Record = {}; + const spent: Record = {}; + const fundedCount: Record = {}; + const spentCount: Record = {}; + for (const vin of tx.vin) { + const address = vin.prevout?.scriptpubkey_address; + if (address && wallet[address]) { + spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0); + spentCount[address] = (spentCount[address] ?? 0) + 1; + } + } + for (const vout of tx.vout) { + const address = vout.scriptpubkey_address; + if (address && wallet[address]) { + funded[address] = (funded[address] ?? 0) + (vout.value ?? 0); + fundedCount[address] = (fundedCount[address] ?? 0) + 1; + } + } + for (const address of Object.keys({ ...funded, ...spent })) { + // add tx to summary + const txSummary: AddressTxSummary = { + txid: tx.txid, + value: (funded[address] ?? 0) - (spent[address] ?? 0), + height: tx.status.block_height, + time: tx.status.block_time, + }; + wallet[address].transactions?.push(txSummary); + newSummaries.push(txSummary); + } + } + return this.deduplicateWalletTransactions([...summaries, ...newSummaries]); + }, this.deduplicateWalletTransactions(Object.values(wallet).flatMap(address => address.transactions))) + )) + ); + + this.walletStats$ = this.wallet$.pipe( + switchMap(wallet => { + const walletStats = new WalletStats(Object.values(wallet).map(w => w.stats), Object.keys(wallet)); + return this.stateService.walletTransactions$.pipe( + startWith([]), + scan((stats, newTransactions) => { + for (const tx of newTransactions) { + stats.addTx(tx); + } + return stats; + }, walletStats), + ); + }), + ); + } + + deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] { + const transactions = new Map(); + for (const tx of walletTransactions) { + if (transactions.has(tx.txid)) { + transactions.get(tx.txid).value += tx.value; + } else { + transactions.set(tx.txid, tx); + } + } + return Array.from(transactions.values()).sort((a, b) => { + if (a.height === b.height) { + return b.tx_position - a.tx_position; + } + return b.height - a.height; + }); + } + + normalizeAddress(address: string): string { + if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(address)) { + return address.toLowerCase(); + } else { + return address; + } + } + + ngOnDestroy(): void { + this.websocketService.stopTrackingWallet(); + this.walletSubscription.unsubscribe(); + } +} diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index 6bedaafb0..79b0118a9 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -1,14 +1,14 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { combineLatest, EMPTY, fromEvent, interval, merge, Observable, of, Subject, Subscription, timer } from 'rxjs'; import { catchError, delayWhen, distinctUntilChanged, filter, map, scan, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators'; -import { AuditStatus, BlockExtended, CurrentPegs, FederationAddress, FederationUtxo, OptimizedMempoolStats, PegsVolume, RecentPeg, TransactionStripped } from '../interfaces/node-api.interface'; -import { MempoolInfo, ReplacementInfo } from '../interfaces/websocket.interface'; -import { ApiService } from '../services/api.service'; -import { StateService } from '../services/state.service'; -import { WebsocketService } from '../services/websocket.service'; -import { SeoService } from '../services/seo.service'; -import { ActiveFilter, FilterMode, GradientMode, toFlags } from '../shared/filters.utils'; -import { detectWebGL } from '../shared/graphs.utils'; +import { AuditStatus, BlockExtended, CurrentPegs, FederationAddress, FederationUtxo, OptimizedMempoolStats, PegsVolume, RecentPeg, TransactionStripped } from '@interfaces/node-api.interface'; +import { MempoolInfo, ReplacementInfo } from '@interfaces/websocket.interface'; +import { ApiService } from '@app/services/api.service'; +import { StateService } from '@app/services/state.service'; +import { WebsocketService } from '@app/services/websocket.service'; +import { SeoService } from '@app/services/seo.service'; +import { ActiveFilter, FilterMode, GradientMode, toFlags } from '@app/shared/filters.utils'; +import { detectWebGL } from '@app/shared/graphs.utils'; interface MempoolBlocksData { blocks: number; diff --git a/frontend/src/app/data-cy.directive.ts b/frontend/src/app/data-cy.directive.ts index 1e8e9aec7..7bee94346 100644 --- a/frontend/src/app/data-cy.directive.ts +++ b/frontend/src/app/data-cy.directive.ts @@ -1,5 +1,5 @@ import { Directive, ElementRef, Renderer2 } from '@angular/core'; -import { environment } from '../environments/environment'; +import { environment } from '@environments/environment'; @Directive({ selector: '[data-cy]' diff --git a/frontend/src/app/docs/api-docs/api-docs-data.ts b/frontend/src/app/docs/api-docs/api-docs-data.ts index 12bb96166..cad4b47bf 100644 --- a/frontend/src/app/docs/api-docs/api-docs-data.ts +++ b/frontend/src/app/docs/api-docs/api-docs-data.ts @@ -9163,11 +9163,13 @@ export const restApiDocsData = [ Filters can be applied:
  • status: all, requested, accelerating, mined, completed, failed
  • timeframe: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y, 4y, all
  • -
  • poolUniqueId: any id from https://github.com/mempool/mining-pools/blob/master/pools-v2.json. Note: This will return all acceleration requests accepted by the pool but the the listed transactions may have been mined by another pool. +
  • minedByPoolUniqueId: any id from pools-v2.json
  • blockHash: a block hash
  • blockHeight: a block height
  • page: the requested page number if using pagination (min: 1)
  • pageLength: the page lenght if using pagination (min: 1, max: 50) +
  • from: unix timestamp (overrides timeframe) +
  • to: unix timestamp (overrides timeframe)

` }, urlString: "/v1/services/accelerator/accelerations/history", @@ -9187,21 +9189,22 @@ export const restApiDocsData = [ headers: '', response: `[ { - "txid": "d7e1796d8eb4a09d4e6c174e36cfd852f1e6e6c9f7df4496339933cd32cbdd1d", - "status": "completed", - "added": 1707421053, - "lastUpdated": 1719134667, - "effectiveFee": 146, - "effectiveVsize": 141, - "feeDelta": 14000, - "blockHash": "00000000000000000000482f0746d62141694b9210a813b97eb8445780a32003", - "blockHeight": 829559, - "bidBoost": 3239, - "boostVersion": "v1", + "txid": "f829900985aad885c13fb90555d27514b05a338202c7ef5d694f4813ad474487", + "status": "completed_provisional", + "added": 1728111527, + "lastUpdated": 1728112113, + "effectiveFee": 1385, + "effectiveVsize": 276, + "feeDelta": 3000, + "blockHash": "00000000000000000000cde89e34036ece454ca2d07ddd7f71ab46307ca87423", + "blockHeight": 864248, + "bidBoost": 65, + "boostVersion": "v2", "pools": [ - 111 + 111, + 115, ], - "minedByPoolUniqueId": 111 + "minedByPoolUniqueId": 115 } ]`, }, diff --git a/frontend/src/app/docs/api-docs/api-docs-nav.component.ts b/frontend/src/app/docs/api-docs/api-docs-nav.component.ts index 1abc506e5..11e39b518 100644 --- a/frontend/src/app/docs/api-docs/api-docs-nav.component.ts +++ b/frontend/src/app/docs/api-docs/api-docs-nav.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; -import { Env, StateService } from '../../services/state.service'; -import { restApiDocsData } from './api-docs-data'; -import { faqData } from './api-docs-data'; +import { Env, StateService } from '@app/services/state.service'; +import { restApiDocsData } from '@app/docs/api-docs/api-docs-data'; +import { faqData } from '@app/docs/api-docs/api-docs-data'; @Component({ selector: 'app-api-docs-nav', diff --git a/frontend/src/app/docs/api-docs/api-docs.component.ts b/frontend/src/app/docs/api-docs/api-docs.component.ts index efdc80d94..0298fc9f3 100644 --- a/frontend/src/app/docs/api-docs/api-docs.component.ts +++ b/frontend/src/app/docs/api-docs/api-docs.component.ts @@ -1,10 +1,10 @@ import { Component, OnInit, Input, QueryList, AfterViewInit, ViewChildren } from '@angular/core'; -import { Env, StateService } from '../../services/state.service'; +import { Env, StateService } from '@app/services/state.service'; import { Observable, merge, of, Subject, Subscription } from 'rxjs'; import { tap, takeUntil } from 'rxjs/operators'; import { ActivatedRoute } from "@angular/router"; -import { faqData, restApiDocsData, wsApiDocsData } from './api-docs-data'; -import { FaqTemplateDirective } from '../faq-template/faq-template.component'; +import { faqData, restApiDocsData, wsApiDocsData } from '@app/docs/api-docs/api-docs-data'; +import { FaqTemplateDirective } from '@app/docs/faq-template/faq-template.component'; @Component({ selector: 'app-api-docs', diff --git a/frontend/src/app/docs/code-template/code-template.component.ts b/frontend/src/app/docs/code-template/code-template.component.ts index b31def01c..f112e23f3 100644 --- a/frontend/src/app/docs/code-template/code-template.component.ts +++ b/frontend/src/app/docs/code-template/code-template.component.ts @@ -1,5 +1,5 @@ import { Component, Input, OnInit } from '@angular/core'; -import { Env, StateService } from '../../services/state.service'; +import { Env, StateService } from '@app/services/state.service'; @Component({ selector: 'app-code-template', diff --git a/frontend/src/app/docs/docs.module.ts b/frontend/src/app/docs/docs.module.ts index 70f22a9a6..7f94135aa 100644 --- a/frontend/src/app/docs/docs.module.ts +++ b/frontend/src/app/docs/docs.module.ts @@ -1,12 +1,12 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { SharedModule } from '../shared/shared.module'; -import { ApiDocsComponent } from './/api-docs/api-docs.component'; -import { DocsComponent } from './docs/docs.component'; -import { ApiDocsNavComponent } from './api-docs/api-docs-nav.component'; -import { CodeTemplateComponent } from './code-template/code-template.component'; -import { DocsRoutingModule } from './docs.routing.module'; -import { FaqTemplateDirective } from './faq-template/faq-template.component'; +import { SharedModule } from '@app/shared/shared.module'; +import { ApiDocsComponent } from '@app/docs/api-docs/api-docs.component'; +import { DocsComponent } from '@app/docs/docs/docs.component'; +import { ApiDocsNavComponent } from '@app/docs/api-docs/api-docs-nav.component'; +import { CodeTemplateComponent } from '@app/docs/code-template/code-template.component'; +import { DocsRoutingModule } from '@app/docs/docs.routing.module'; +import { FaqTemplateDirective } from '@app/docs/faq-template/faq-template.component'; @NgModule({ declarations: [ ApiDocsComponent, diff --git a/frontend/src/app/docs/docs.routing.module.ts b/frontend/src/app/docs/docs.routing.module.ts index 3c0eb961d..7185a7181 100644 --- a/frontend/src/app/docs/docs.routing.module.ts +++ b/frontend/src/app/docs/docs.routing.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { DocsComponent } from './docs/docs.component'; +import { DocsComponent } from '@app/docs/docs/docs.component'; const browserWindow = window || {}; // @ts-ignore diff --git a/frontend/src/app/docs/docs/docs.component.ts b/frontend/src/app/docs/docs/docs.component.ts index 35080a19f..6d3ff4723 100644 --- a/frontend/src/app/docs/docs/docs.component.ts +++ b/frontend/src/app/docs/docs/docs.component.ts @@ -1,9 +1,9 @@ import { Component, OnInit, HostBinding } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { Env, StateService } from '../../services/state.service'; -import { WebsocketService } from '../../services/websocket.service'; -import { SeoService } from '../../services/seo.service'; -import { OpenGraphService } from '../../services/opengraph.service'; +import { Env, StateService } from '@app/services/state.service'; +import { WebsocketService } from '@app/services/websocket.service'; +import { SeoService } from '@app/services/seo.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; @Component({ selector: 'app-docs', diff --git a/frontend/src/app/fiat/fiat.component.ts b/frontend/src/app/fiat/fiat.component.ts index 93752039b..95f019723 100644 --- a/frontend/src/app/fiat/fiat.component.ts +++ b/frontend/src/app/fiat/fiat.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy } from '@angular/core'; import { Observable, Subscription } from 'rxjs'; -import { Price } from '../services/price.service'; -import { StateService } from '../services/state.service'; +import { Price } from '@app/services/price.service'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-fiat', diff --git a/frontend/src/app/graphs/echarts.ts b/frontend/src/app/graphs/echarts.ts index 74fec1e71..67ed7e3b8 100644 --- a/frontend/src/app/graphs/echarts.ts +++ b/frontend/src/app/graphs/echarts.ts @@ -1,6 +1,6 @@ // Import tree-shakeable echarts import * as echarts from 'echarts/core'; -import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart } from 'echarts/charts'; +import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart, CustomChart } from 'echarts/charts'; import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent } from 'echarts/components'; import { SVGRenderer, CanvasRenderer } from 'echarts/renderers'; // Typescript interfaces @@ -12,6 +12,7 @@ echarts.use([ TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent, - LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart + LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart, + CustomChart, ]); export { echarts, EChartsOption, TreemapSeriesOption, LineSeriesOption, PieSeriesOption }; \ No newline at end of file diff --git a/frontend/src/app/graphs/graphs.module.ts b/frontend/src/app/graphs/graphs.module.ts index de048fd2d..4e6b00637 100644 --- a/frontend/src/app/graphs/graphs.module.ts +++ b/frontend/src/app/graphs/graphs.module.ts @@ -1,42 +1,45 @@ import { NgModule } from '@angular/core'; import { NgxEchartsModule } from 'ngx-echarts'; -import { GraphsRoutingModule } from './graphs.routing.module'; -import { SharedModule } from '../shared/shared.module'; +import { GraphsRoutingModule } from '@app/graphs/graphs.routing.module'; +import { SharedModule } from '@app/shared/shared.module'; -import { AccelerationFeesGraphComponent } from '../components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component'; -import { BlockFeesGraphComponent } from '../components/block-fees-graph/block-fees-graph.component'; -import { BlockFeesSubsidyGraphComponent } from '../components/block-fees-subsidy-graph/block-fees-subsidy-graph.component'; -import { BlockRewardsGraphComponent } from '../components/block-rewards-graph/block-rewards-graph.component'; -import { BlockFeeRatesGraphComponent } from '../components/block-fee-rates-graph/block-fee-rates-graph.component'; -import { BlockSizesWeightsGraphComponent } from '../components/block-sizes-weights-graph/block-sizes-weights-graph.component'; -import { FeeDistributionGraphComponent } from '../components/fee-distribution-graph/fee-distribution-graph.component'; -import { IncomingTransactionsGraphComponent } from '../components/incoming-transactions-graph/incoming-transactions-graph.component'; -import { MempoolGraphComponent } from '../components/mempool-graph/mempool-graph.component'; -import { LbtcPegsGraphComponent } from '../components/lbtc-pegs-graph/lbtc-pegs-graph.component'; -import { ReservesSupplyStatsComponent } from '../components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component'; -import { ExpiredUtxosStatsComponent } from '../components/liquid-reserves-audit/expired-utxos-stats/expired-utxos-stats.component'; -import { ReservesRatioStatsComponent } from '../components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component'; -import { ReservesRatioComponent } from '../components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component'; -import { RecentPegsStatsComponent } from '../components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component'; -import { RecentPegsListComponent } from '../components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component'; -import { FederationAddressesStatsComponent } from '../components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component'; -import { FederationAddressesListComponent } from '../components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component'; -import { GraphsComponent } from '../components/graphs/graphs.component'; -import { StatisticsComponent } from '../components/statistics/statistics.component'; -import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.component'; -import { PoolRankingComponent } from '../components/pool-ranking/pool-ranking.component'; -import { PoolComponent } from '../components/pool/pool.component'; -import { TelevisionComponent } from '../components/television/television.component'; -import { DashboardComponent } from '../dashboard/dashboard.component'; -import { CustomDashboardComponent } from '../components/custom-dashboard/custom-dashboard.component'; -import { MiningDashboardComponent } from '../components/mining-dashboard/mining-dashboard.component'; -import { AcceleratorDashboardComponent } from '../components/acceleration/accelerator-dashboard/accelerator-dashboard.component'; -import { HashrateChartComponent } from '../components/hashrate-chart/hashrate-chart.component'; -import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools/hashrate-chart-pools.component'; -import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component'; -import { AddressComponent } from '../components/address/address.component'; -import { AddressGraphComponent } from '../components/address-graph/address-graph.component'; -import { ActiveAccelerationBox } from '../components/acceleration/active-acceleration-box/active-acceleration-box.component'; +import { AccelerationFeesGraphComponent } from '@components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component'; +import { BlockFeesGraphComponent } from '@components/block-fees-graph/block-fees-graph.component'; +import { BlockFeesSubsidyGraphComponent } from '@components/block-fees-subsidy-graph/block-fees-subsidy-graph.component'; +import { BlockRewardsGraphComponent } from '@components/block-rewards-graph/block-rewards-graph.component'; +import { BlockFeeRatesGraphComponent } from '@components/block-fee-rates-graph/block-fee-rates-graph.component'; +import { BlockSizesWeightsGraphComponent } from '@components/block-sizes-weights-graph/block-sizes-weights-graph.component'; +import { FeeDistributionGraphComponent } from '@components/fee-distribution-graph/fee-distribution-graph.component'; +import { IncomingTransactionsGraphComponent } from '@components/incoming-transactions-graph/incoming-transactions-graph.component'; +import { MempoolGraphComponent } from '@components/mempool-graph/mempool-graph.component'; +import { LbtcPegsGraphComponent } from '@components/lbtc-pegs-graph/lbtc-pegs-graph.component'; +import { ReservesSupplyStatsComponent } from '@components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component'; +import { ExpiredUtxosStatsComponent } from '@components/liquid-reserves-audit/expired-utxos-stats/expired-utxos-stats.component'; +import { ReservesRatioStatsComponent } from '@components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component'; +import { ReservesRatioComponent } from '@components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component'; +import { RecentPegsStatsComponent } from '@components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component'; +import { RecentPegsListComponent } from '@components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component'; +import { FederationAddressesStatsComponent } from '@components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component'; +import { FederationAddressesListComponent } from '@components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component'; +import { GraphsComponent } from '@components/graphs/graphs.component'; +import { StatisticsComponent } from '@components/statistics/statistics.component'; +import { MempoolBlockComponent } from '@components/mempool-block/mempool-block.component'; +import { PoolRankingComponent } from '@components/pool-ranking/pool-ranking.component'; +import { PoolComponent } from '@components/pool/pool.component'; +import { TelevisionComponent } from '@components/television/television.component'; +import { DashboardComponent } from '@app/dashboard/dashboard.component'; +import { CustomDashboardComponent } from '@components/custom-dashboard/custom-dashboard.component'; +import { MiningDashboardComponent } from '@components/mining-dashboard/mining-dashboard.component'; +import { AcceleratorDashboardComponent } from '@components/acceleration/accelerator-dashboard/accelerator-dashboard.component'; +import { HashrateChartComponent } from '@components/hashrate-chart/hashrate-chart.component'; +import { HashrateChartPoolsComponent } from '@components/hashrates-chart-pools/hashrate-chart-pools.component'; +import { BlockHealthGraphComponent } from '@components/block-health-graph/block-health-graph.component'; +import { AddressComponent } from '@components/address/address.component'; +import { WalletComponent } from '@components/wallet/wallet.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 { AddressesTreemap } from '@components/addresses-treemap/addresses-treemap.component'; import { CommonModule } from '@angular/common'; @NgModule({ @@ -45,6 +48,7 @@ import { CommonModule } from '@angular/common'; CustomDashboardComponent, MempoolBlockComponent, AddressComponent, + WalletComponent, MiningDashboardComponent, AcceleratorDashboardComponent, @@ -76,14 +80,16 @@ import { CommonModule } from '@angular/common'; HashrateChartPoolsComponent, BlockHealthGraphComponent, AddressGraphComponent, + UtxoGraphComponent, ActiveAccelerationBox, + AddressesTreemap, ], imports: [ CommonModule, SharedModule, GraphsRoutingModule, NgxEchartsModule.forRoot({ - echarts: () => import('./echarts').then(m => m.echarts), + echarts: () => import('@app/graphs/echarts').then(m => m.echarts), }) ], exports: [ diff --git a/frontend/src/app/graphs/graphs.routing.module.ts b/frontend/src/app/graphs/graphs.routing.module.ts index 40bf64144..886d55072 100644 --- a/frontend/src/app/graphs/graphs.routing.module.ts +++ b/frontend/src/app/graphs/graphs.routing.module.ts @@ -1,27 +1,28 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component'; -import { BlockFeeRatesGraphComponent } from '../components/block-fee-rates-graph/block-fee-rates-graph.component'; -import { BlockFeesGraphComponent } from '../components/block-fees-graph/block-fees-graph.component'; -import { BlockFeesSubsidyGraphComponent } from '../components/block-fees-subsidy-graph/block-fees-subsidy-graph.component'; -import { BlockRewardsGraphComponent } from '../components/block-rewards-graph/block-rewards-graph.component'; -import { BlockSizesWeightsGraphComponent } from '../components/block-sizes-weights-graph/block-sizes-weights-graph.component'; -import { GraphsComponent } from '../components/graphs/graphs.component'; -import { HashrateChartComponent } from '../components/hashrate-chart/hashrate-chart.component'; -import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools/hashrate-chart-pools.component'; -import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.component'; -import { MiningDashboardComponent } from '../components/mining-dashboard/mining-dashboard.component'; -import { AcceleratorDashboardComponent } from '../components/acceleration/accelerator-dashboard/accelerator-dashboard.component'; -import { PoolRankingComponent } from '../components/pool-ranking/pool-ranking.component'; -import { PoolComponent } from '../components/pool/pool.component'; -import { StartComponent } from '../components/start/start.component'; -import { StatisticsComponent } from '../components/statistics/statistics.component'; -import { TelevisionComponent } from '../components/television/television.component'; -import { DashboardComponent } from '../dashboard/dashboard.component'; -import { CustomDashboardComponent } from '../components/custom-dashboard/custom-dashboard.component'; -import { AccelerationFeesGraphComponent } from '../components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component'; -import { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component'; -import { AddressComponent } from '../components/address/address.component'; +import { BlockHealthGraphComponent } from '@components/block-health-graph/block-health-graph.component'; +import { BlockFeeRatesGraphComponent } from '@components/block-fee-rates-graph/block-fee-rates-graph.component'; +import { BlockFeesGraphComponent } from '@components/block-fees-graph/block-fees-graph.component'; +import { BlockFeesSubsidyGraphComponent } from '@components/block-fees-subsidy-graph/block-fees-subsidy-graph.component'; +import { BlockRewardsGraphComponent } from '@components/block-rewards-graph/block-rewards-graph.component'; +import { BlockSizesWeightsGraphComponent } from '@components/block-sizes-weights-graph/block-sizes-weights-graph.component'; +import { GraphsComponent } from '@components/graphs/graphs.component'; +import { HashrateChartComponent } from '@components/hashrate-chart/hashrate-chart.component'; +import { HashrateChartPoolsComponent } from '@components/hashrates-chart-pools/hashrate-chart-pools.component'; +import { MempoolBlockComponent } from '@components/mempool-block/mempool-block.component'; +import { MiningDashboardComponent } from '@components/mining-dashboard/mining-dashboard.component'; +import { AcceleratorDashboardComponent } from '@components/acceleration/accelerator-dashboard/accelerator-dashboard.component'; +import { PoolRankingComponent } from '@components/pool-ranking/pool-ranking.component'; +import { PoolComponent } from '@components/pool/pool.component'; +import { StartComponent } from '@components/start/start.component'; +import { StatisticsComponent } from '@components/statistics/statistics.component'; +import { TelevisionComponent } from '@components/television/television.component'; +import { DashboardComponent } from '@app/dashboard/dashboard.component'; +import { CustomDashboardComponent } from '@components/custom-dashboard/custom-dashboard.component'; +import { AccelerationFeesGraphComponent } from '@components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component'; +import { AccelerationsListComponent } from '@components/acceleration/accelerations-list/accelerations-list.component'; +import { AddressComponent } from '@components/address/address.component'; +import { WalletComponent } from '@components/wallet/wallet.component'; const browserWindow = window || {}; // @ts-ignore @@ -88,6 +89,15 @@ const routes: Routes = [ networkSpecific: true, } }, + { + path: 'wallet/:wallet', + children: [], + component: WalletComponent, + data: { + ogImage: true, + networkSpecific: true, + } + }, { path: 'graphs', data: { networks: ['bitcoin', 'liquid'] }, @@ -146,7 +156,7 @@ const routes: Routes = [ { path: 'lightning', data: { preload: true, networks: ['bitcoin'] }, - loadChildren: () => import ('./lightning-graphs.module').then(m => m.LightningGraphsModule), + loadChildren: () => import ('@app/graphs/lightning-graphs.module').then(m => m.LightningGraphsModule), }, { path: '', diff --git a/frontend/src/app/graphs/lightning-graphs.module.ts b/frontend/src/app/graphs/lightning-graphs.module.ts index ac123be33..869ce94af 100644 --- a/frontend/src/app/graphs/lightning-graphs.module.ts +++ b/frontend/src/app/graphs/lightning-graphs.module.ts @@ -1,13 +1,13 @@ import { NgModule } from '@angular/core'; -import { SharedModule } from '../shared/shared.module'; +import { SharedModule } from '@app/shared/shared.module'; import { CommonModule } from '@angular/common'; import { RouterModule, Routes } from '@angular/router'; -import { NodesNetworksChartComponent } from '../lightning/nodes-networks-chart/nodes-networks-chart.component'; -import { LightningStatisticsChartComponent } from '../lightning/statistics-chart/lightning-statistics-chart.component'; -import { NodesPerISPChartComponent } from '../lightning/nodes-per-isp-chart/nodes-per-isp-chart.component'; -import { NodesPerCountryChartComponent } from '../lightning/nodes-per-country-chart/nodes-per-country-chart.component'; -import { NodesMap } from '../lightning/nodes-map/nodes-map.component'; -import { NodesChannelsMap } from '../lightning/nodes-channels-map/nodes-channels-map.component'; +import { NodesNetworksChartComponent } from '@app/lightning/nodes-networks-chart/nodes-networks-chart.component'; +import { LightningStatisticsChartComponent } from '@app/lightning/statistics-chart/lightning-statistics-chart.component'; +import { NodesPerISPChartComponent } from '@app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component'; +import { NodesPerCountryChartComponent } from '@app/lightning/nodes-per-country-chart/nodes-per-country-chart.component'; +import { NodesMap } from '@app/lightning/nodes-map/nodes-map.component'; +import { NodesChannelsMap } from '@app/lightning/nodes-channels-map/nodes-channels-map.component'; const routes: Routes = [ { diff --git a/frontend/src/app/interfaces/electrs.interface.ts b/frontend/src/app/interfaces/electrs.interface.ts index b32a2aae6..5a707d889 100644 --- a/frontend/src/app/interfaces/electrs.interface.ts +++ b/frontend/src/app/interfaces/electrs.interface.ts @@ -1,5 +1,5 @@ -import { Price } from '../services/price.service'; -import { IChannel } from './node-api.interface'; +import { Price } from '@app/services/price.service'; +import { IChannel } from '@interfaces/node-api.interface'; export interface Transaction { txid: string; @@ -74,6 +74,8 @@ export interface Vin { issuance?: Issuance; // Custom lazy?: boolean; + // Ord + isInscription?: boolean; } interface Issuance { @@ -98,6 +100,8 @@ export interface Vout { valuecommitment?: number; asset?: string; pegout?: Pegout; + // Ord + isRunestone?: boolean; } interface Pegout { @@ -160,6 +164,7 @@ export interface AddressTxSummary { height: number; time: number; price?: number; + tx_position?: number; } export interface ChainStats { @@ -233,3 +238,10 @@ interface AssetStats { peg_out_amount: number; burn_count: number; } + +export interface Utxo { + txid: string; + vout: number; + value: number; + status: Status; +} diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 3e38ff88b..0091262e1 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -1,4 +1,4 @@ -import { Block, Transaction } from "./electrs.interface"; +import { AddressTxSummary, Block, ChainStats, Transaction } from "./electrs.interface"; export interface OptimizedMempoolStats { added: number; @@ -203,6 +203,7 @@ export interface BlockExtension { id: number; name: string; slug: string; + minerNames: string[] | null; } } @@ -451,4 +452,28 @@ export interface TestMempoolAcceptResult { "effective-includes": string[], }, ['reject-reason']?: string, -} \ No newline at end of file +} + +export interface SubmitPackageResult { + package_msg: string; + "tx-results": { [wtxid: string]: TxResult }; + "replaced-transactions"?: string[]; +} + +export interface TxResult { + txid: string; + "other-wtxid"?: string; + vsize?: number; + fees?: { + base: number; + "effective-feerate"?: number; + "effective-includes"?: string[]; + }; + error?: string; +} +export interface WalletAddress { + address: string; + active: boolean; + stats: ChainStats; + transactions: AddressTxSummary[]; +} diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index 7552224f5..89c8e3884 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -1,7 +1,7 @@ import { SafeResourceUrl } from '@angular/platform-browser'; -import { ILoadingIndicators } from '../services/state.service'; -import { Transaction } from './electrs.interface'; -import { Acceleration, BlockExtended, DifficultyAdjustment, RbfTree, TransactionStripped } from './node-api.interface'; +import { ILoadingIndicators } from '@app/services/state.service'; +import { Transaction } from '@interfaces/electrs.interface'; +import { Acceleration, BlockExtended, DifficultyAdjustment, RbfTree, TransactionStripped } from '@interfaces/node-api.interface'; export interface WebsocketResponse { backend?: 'esplora' | 'electrum' | 'none'; @@ -36,6 +36,7 @@ export interface WebsocketResponse { 'track-rbf'?: string; 'track-rbf-summary'?: boolean; 'track-accelerations'?: boolean; + 'track-wallet'?: string; 'watch-mempool'?: boolean; 'refresh-blocks'?: boolean; } @@ -143,4 +144,4 @@ export interface HealthCheckHost { link?: string; statusPage?: SafeResourceUrl; flag?: string; -} \ No newline at end of file +} diff --git a/frontend/src/app/lightning/channel/channel-box/channel-box.component.spec.ts b/frontend/src/app/lightning/channel/channel-box/channel-box.component.spec.ts index ae9463a6c..22dd36f75 100644 --- a/frontend/src/app/lightning/channel/channel-box/channel-box.component.spec.ts +++ b/frontend/src/app/lightning/channel/channel-box/channel-box.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ChannelBoxComponent } from './channel-box.component'; +import { ChannelBoxComponent } from '@components/channel-box.component'; describe('ChannelBoxComponent', () => { let component: ChannelBoxComponent; diff --git a/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.spec.ts b/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.spec.ts index eea4ee99c..9a63d7f59 100644 --- a/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.spec.ts +++ b/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ChannelCloseBoxComponent } from './channel-close-box.component'; +import { ChannelCloseBoxComponent } from '@components/channel-close-box.component'; describe('ChannelCloseBoxComponent', () => { let component: ChannelCloseBoxComponent; diff --git a/frontend/src/app/lightning/channel/channel-preview.component.ts b/frontend/src/app/lightning/channel/channel-preview.component.ts index 7e3152513..84a85f9c6 100644 --- a/frontend/src/app/lightning/channel/channel-preview.component.ts +++ b/frontend/src/app/lightning/channel/channel-preview.component.ts @@ -2,9 +2,9 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { Observable, of } from 'rxjs'; import { catchError, switchMap, tap } from 'rxjs/operators'; -import { SeoService } from '../../services/seo.service'; -import { OpenGraphService } from '../../services/opengraph.service'; -import { LightningApiService } from '../lightning-api.service'; +import { SeoService } from '@app/services/seo.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; @Component({ selector: 'app-channel-preview', diff --git a/frontend/src/app/lightning/channel/channel.component.ts b/frontend/src/app/lightning/channel/channel.component.ts index a26101bdb..501fc74f6 100644 --- a/frontend/src/app/lightning/channel/channel.component.ts +++ b/frontend/src/app/lightning/channel/channel.component.ts @@ -2,10 +2,10 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { Observable, of, zip } from 'rxjs'; import { catchError, map, shareReplay, switchMap, tap } from 'rxjs/operators'; -import { IChannel } from '../../interfaces/node-api.interface'; -import { ElectrsApiService } from '../../services/electrs-api.service'; -import { SeoService } from '../../services/seo.service'; -import { LightningApiService } from '../lightning-api.service'; +import { IChannel } from '@interfaces/node-api.interface'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; +import { SeoService } from '@app/services/seo.service'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; @Component({ selector: 'app-channel', diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.ts b/frontend/src/app/lightning/channels-list/channels-list.component.ts index d083178c0..3439bb2b4 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.ts +++ b/frontend/src/app/lightning/channels-list/channels-list.component.ts @@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnI import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { BehaviorSubject, merge, Observable } from 'rxjs'; import { map, switchMap, tap } from 'rxjs/operators'; -import { isMobile } from '../../shared/common.utils'; -import { LightningApiService } from '../lightning-api.service'; +import { isMobile } from '@app/shared/common.utils'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; @Component({ selector: 'app-channels-list', diff --git a/frontend/src/app/lightning/channels-statistics/channels-statistics.component.ts b/frontend/src/app/lightning/channels-statistics/channels-statistics.component.ts index f2b78f53c..ee3e13d10 100644 --- a/frontend/src/app/lightning/channels-statistics/channels-statistics.component.ts +++ b/frontend/src/app/lightning/channels-statistics/channels-statistics.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; -import { INodesStatistics } from '../../interfaces/node-api.interface'; +import { INodesStatistics } from '@interfaces/node-api.interface'; @Component({ selector: 'app-channels-statistics', diff --git a/frontend/src/app/lightning/group/group-preview.component.ts b/frontend/src/app/lightning/group/group-preview.component.ts index 35bcb6e0f..4b8f5ed77 100644 --- a/frontend/src/app/lightning/group/group-preview.component.ts +++ b/frontend/src/app/lightning/group/group-preview.component.ts @@ -1,10 +1,10 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { map, switchMap, Observable, catchError, of } from 'rxjs'; -import { SeoService } from '../../services/seo.service'; -import { OpenGraphService } from '../../services/opengraph.service'; -import { GeolocationData } from '../../shared/components/geolocation/geolocation.component'; -import { LightningApiService } from '../lightning-api.service'; +import { SeoService } from '@app/services/seo.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; +import { GeolocationData } from '@app/shared/components/geolocation/geolocation.component'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; interface NodeGroup { name: string; diff --git a/frontend/src/app/lightning/group/group.component.ts b/frontend/src/app/lightning/group/group.component.ts index ea83653c2..76f9ef080 100644 --- a/frontend/src/app/lightning/group/group.component.ts +++ b/frontend/src/app/lightning/group/group.component.ts @@ -1,9 +1,9 @@ import { Component, OnInit } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { map, Observable, share } from 'rxjs'; -import { SeoService } from '../../services/seo.service'; -import { GeolocationData } from '../../shared/components/geolocation/geolocation.component'; -import { LightningApiService } from '../lightning-api.service'; +import { SeoService } from '@app/services/seo.service'; +import { GeolocationData } from '@app/shared/components/geolocation/geolocation.component'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; @Component({ selector: 'app-group', diff --git a/frontend/src/app/lightning/justice-list/justice-list.component.ts b/frontend/src/app/lightning/justice-list/justice-list.component.ts index d48cdb7ea..e075aed93 100644 --- a/frontend/src/app/lightning/justice-list/justice-list.component.ts +++ b/frontend/src/app/lightning/justice-list/justice-list.component.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { map, Observable, of, Subject, Subscription, switchMap, tap, zip } from 'rxjs'; -import { IChannel } from '../../interfaces/node-api.interface'; -import { LightningApiService } from '../lightning-api.service'; -import { Transaction } from '../../interfaces/electrs.interface'; -import { ElectrsApiService } from '../../services/electrs-api.service'; +import { IChannel } from '@interfaces/node-api.interface'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; +import { Transaction } from '@interfaces/electrs.interface'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; @Component({ selector: 'app-justice-list', diff --git a/frontend/src/app/lightning/lightning-api.service.ts b/frontend/src/app/lightning/lightning-api.service.ts index 74f048d68..14276dc12 100644 --- a/frontend/src/app/lightning/lightning-api.service.ts +++ b/frontend/src/app/lightning/lightning-api.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { BehaviorSubject, Observable, catchError, filter, of, shareReplay, take, tap } from 'rxjs'; -import { StateService } from '../services/state.service'; -import { IChannel, INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesPerChannels } from '../interfaces/node-api.interface'; +import { StateService } from '@app/services/state.service'; +import { IChannel, INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesPerChannels } from '@interfaces/node-api.interface'; @Injectable({ providedIn: 'root' diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts index fd72cddfe..47b7700a4 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts @@ -1,11 +1,11 @@ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, OnInit } from '@angular/core'; import { Observable, merge } from 'rxjs'; import { share } from 'rxjs/operators'; -import { INodesRanking, INodesStatistics } from '../../interfaces/node-api.interface'; -import { SeoService } from '../../services/seo.service'; -import { OpenGraphService } from '../../services/opengraph.service'; -import { StateService } from '../../services/state.service'; -import { LightningApiService } from '../lightning-api.service'; +import { INodesRanking, INodesStatistics } from '@interfaces/node-api.interface'; +import { SeoService } from '@app/services/seo.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; +import { StateService } from '@app/services/state.service'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; @Component({ selector: 'app-lightning-dashboard', diff --git a/frontend/src/app/lightning/lightning-previews.module.ts b/frontend/src/app/lightning/lightning-previews.module.ts index c41ba8d20..5efac798f 100644 --- a/frontend/src/app/lightning/lightning-previews.module.ts +++ b/frontend/src/app/lightning/lightning-previews.module.ts @@ -1,15 +1,15 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { SharedModule } from '../shared/shared.module'; +import { SharedModule } from '@app/shared/shared.module'; import { RouterModule } from '@angular/router'; -import { GraphsModule } from '../graphs/graphs.module'; -import { LightningModule } from './lightning.module'; -import { LightningApiService } from './lightning-api.service'; -import { NodePreviewComponent } from './node/node-preview.component'; -import { LightningPreviewsRoutingModule } from './lightning-previews.routing.module'; -import { ChannelPreviewComponent } from './channel/channel-preview.component'; -import { NodesPerISPPreview } from './nodes-per-isp/nodes-per-isp-preview.component'; -import { GroupPreviewComponent } from './group/group-preview.component'; +import { GraphsModule } from '@app/graphs/graphs.module'; +import { LightningModule } from '@app/lightning/lightning.module'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; +import { NodePreviewComponent } from '@app/lightning/node/node-preview.component'; +import { LightningPreviewsRoutingModule } from '@app/lightning/lightning-previews.routing.module'; +import { ChannelPreviewComponent } from '@app/lightning/channel/channel-preview.component'; +import { NodesPerISPPreview } from '@app/lightning/nodes-per-isp/nodes-per-isp-preview.component'; +import { GroupPreviewComponent } from '@app/lightning/group/group-preview.component'; @NgModule({ declarations: [ NodePreviewComponent, diff --git a/frontend/src/app/lightning/lightning-previews.routing.module.ts b/frontend/src/app/lightning/lightning-previews.routing.module.ts index 6cce90766..d899e559b 100644 --- a/frontend/src/app/lightning/lightning-previews.routing.module.ts +++ b/frontend/src/app/lightning/lightning-previews.routing.module.ts @@ -1,9 +1,9 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { NodePreviewComponent } from './node/node-preview.component'; -import { ChannelPreviewComponent } from './channel/channel-preview.component'; -import { NodesPerISPPreview } from './nodes-per-isp/nodes-per-isp-preview.component'; -import { GroupPreviewComponent } from './group/group-preview.component'; +import { NodePreviewComponent } from '@app/lightning/node/node-preview.component'; +import { ChannelPreviewComponent } from '@app/lightning/channel/channel-preview.component'; +import { NodesPerISPPreview } from '@app/lightning/nodes-per-isp/nodes-per-isp-preview.component'; +import { GroupPreviewComponent } from '@app/lightning/group/group-preview.component'; const routes: Routes = [ { diff --git a/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.ts b/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.ts index 11bf4dc5d..220e7eebd 100644 --- a/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.ts +++ b/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.ts @@ -1,5 +1,7 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { WebsocketService } from '../../services/websocket.service'; +import { WebsocketService } from '@app/services/websocket.service'; +import { Router, ActivatedRoute } from '@angular/router'; +import { handleDemoRedirect } from '../../shared/common.utils'; @Component({ selector: 'app-lightning-wrapper', @@ -11,10 +13,14 @@ export class LightningWrapperComponent implements OnInit { constructor( private websocketService: WebsocketService, + private router: Router, + private route: ActivatedRoute ) { } ngOnInit() { this.websocketService.want(['blocks']); + + handleDemoRedirect(this.route, this.router); } } diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts index 0b824ad78..405900e19 100644 --- a/frontend/src/app/lightning/lightning.module.ts +++ b/frontend/src/app/lightning/lightning.module.ts @@ -1,39 +1,39 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { SharedModule } from '../shared/shared.module'; -import { LightningDashboardComponent } from './lightning-dashboard/lightning-dashboard.component'; -import { LightningApiService } from './lightning-api.service'; -import { NodesListComponent } from './nodes-list/nodes-list.component'; +import { SharedModule } from '@app/shared/shared.module'; +import { LightningDashboardComponent } from '@app/lightning/lightning-dashboard/lightning-dashboard.component'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; +import { NodesListComponent } from '@app/lightning/nodes-list/nodes-list.component'; import { RouterModule } from '@angular/router'; -import { NodeStatisticsComponent } from './node-statistics/node-statistics.component'; -import { NodeComponent } from './node/node.component'; -import { LightningRoutingModule } from './lightning.routing.module'; -import { ChannelsListComponent } from './channels-list/channels-list.component'; -import { ChannelComponent } from './channel/channel.component'; -import { LightningWrapperComponent } from './lightning-wrapper/lightning-wrapper.component'; -import { ChannelBoxComponent } from './channel/channel-box/channel-box.component'; -import { ChannelCloseBoxComponent } from './channel/channel-close-box/channel-close-box.component'; -import { ClosingTypeComponent } from './channel/closing-type/closing-type.component'; -import { LightningStatisticsChartComponent } from './statistics-chart/lightning-statistics-chart.component'; -import { NodeStatisticsChartComponent } from './node-statistics-chart/node-statistics-chart.component'; -import { NodeFeeChartComponent } from './node-fee-chart/node-fee-chart.component'; -import { GraphsModule } from '../graphs/graphs.module'; -import { NodesNetworksChartComponent } from './nodes-networks-chart/nodes-networks-chart.component'; -import { ChannelsStatisticsComponent } from './channels-statistics/channels-statistics.component'; -import { NodesPerISPChartComponent } from './nodes-per-isp-chart/nodes-per-isp-chart.component'; -import { NodesPerCountry } from './nodes-per-country/nodes-per-country.component'; -import { NodesPerISP } from './nodes-per-isp/nodes-per-isp.component'; -import { NodesPerCountryChartComponent } from '../lightning/nodes-per-country-chart/nodes-per-country-chart.component'; -import { NodesMap } from '../lightning/nodes-map/nodes-map.component'; -import { NodesChannelsMap } from '../lightning/nodes-channels-map/nodes-channels-map.component'; -import { NodesRanking } from '../lightning/nodes-ranking/nodes-ranking.component'; -import { TopNodesPerChannels } from '../lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component'; -import { TopNodesPerCapacity } from '../lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component'; -import { JusticeList } from '../lightning/justice-list/justice-list.component'; -import { OldestNodes } from '../lightning/nodes-ranking/oldest-nodes/oldest-nodes.component'; -import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component'; -import { NodeChannels } from '../lightning/nodes-channels/node-channels.component'; -import { GroupComponent } from './group/group.component'; +import { NodeStatisticsComponent } from '@app/lightning/node-statistics/node-statistics.component'; +import { NodeComponent } from '@app/lightning/node/node.component'; +import { LightningRoutingModule } from '@app/lightning/lightning.routing.module'; +import { ChannelsListComponent } from '@app/lightning/channels-list/channels-list.component'; +import { ChannelComponent } from '@app/lightning/channel/channel.component'; +import { LightningWrapperComponent } from '@app/lightning/lightning-wrapper/lightning-wrapper.component'; +import { ChannelBoxComponent } from '@app/lightning/channel/channel-box/channel-box.component'; +import { ChannelCloseBoxComponent } from '@app/lightning/channel/channel-close-box/channel-close-box.component'; +import { ClosingTypeComponent } from '@app/lightning/channel/closing-type/closing-type.component'; +import { LightningStatisticsChartComponent } from '@app/lightning/statistics-chart/lightning-statistics-chart.component'; +import { NodeStatisticsChartComponent } from '@app/lightning/node-statistics-chart/node-statistics-chart.component'; +import { NodeFeeChartComponent } from '@app/lightning/node-fee-chart/node-fee-chart.component'; +import { GraphsModule } from '@app/graphs/graphs.module'; +import { NodesNetworksChartComponent } from '@app/lightning/nodes-networks-chart/nodes-networks-chart.component'; +import { ChannelsStatisticsComponent } from '@app/lightning/channels-statistics/channels-statistics.component'; +import { NodesPerISPChartComponent } from '@app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component'; +import { NodesPerCountry } from '@app/lightning/nodes-per-country/nodes-per-country.component'; +import { NodesPerISP } from '@app/lightning/nodes-per-isp/nodes-per-isp.component'; +import { NodesPerCountryChartComponent } from '@app/lightning/nodes-per-country-chart/nodes-per-country-chart.component'; +import { NodesMap } from '@app/lightning/nodes-map/nodes-map.component'; +import { NodesChannelsMap } from '@app/lightning/nodes-channels-map/nodes-channels-map.component'; +import { NodesRanking } from '@app/lightning/nodes-ranking/nodes-ranking.component'; +import { TopNodesPerChannels } from '@app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component'; +import { TopNodesPerCapacity } from '@app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component'; +import { JusticeList } from '@app/lightning/justice-list/justice-list.component'; +import { OldestNodes } from '@app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component'; +import { NodesRankingsDashboard } from '@app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component'; +import { NodeChannels } from '@app/lightning/nodes-channels/node-channels.component'; +import { GroupComponent } from '@app/lightning/group/group.component'; @NgModule({ declarations: [ diff --git a/frontend/src/app/lightning/lightning.routing.module.ts b/frontend/src/app/lightning/lightning.routing.module.ts index 8b8041181..c7f167308 100644 --- a/frontend/src/app/lightning/lightning.routing.module.ts +++ b/frontend/src/app/lightning/lightning.routing.module.ts @@ -1,15 +1,15 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { LightningDashboardComponent } from './lightning-dashboard/lightning-dashboard.component'; -import { LightningWrapperComponent } from './lightning-wrapper/lightning-wrapper.component'; -import { NodeComponent } from './node/node.component'; -import { ChannelComponent } from './channel/channel.component'; -import { NodesPerCountry } from './nodes-per-country/nodes-per-country.component'; -import { NodesPerISP } from './nodes-per-isp/nodes-per-isp.component'; -import { NodesRanking } from './nodes-ranking/nodes-ranking.component'; -import { NodesRankingsDashboard } from './nodes-rankings-dashboard/nodes-rankings-dashboard.component'; -import { GroupComponent } from './group/group.component'; -import { JusticeList } from './justice-list/justice-list.component'; +import { LightningDashboardComponent } from '@app/lightning/lightning-dashboard/lightning-dashboard.component'; +import { LightningWrapperComponent } from '@app/lightning/lightning-wrapper/lightning-wrapper.component'; +import { NodeComponent } from '@app/lightning/node/node.component'; +import { ChannelComponent } from '@app/lightning/channel/channel.component'; +import { NodesPerCountry } from '@app/lightning/nodes-per-country/nodes-per-country.component'; +import { NodesPerISP } from '@app/lightning/nodes-per-isp/nodes-per-isp.component'; +import { NodesRanking } from '@app/lightning/nodes-ranking/nodes-ranking.component'; +import { NodesRankingsDashboard } from '@app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component'; +import { GroupComponent } from '@app/lightning/group/group.component'; +import { JusticeList } from '@app/lightning/justice-list/justice-list.component'; const routes: Routes = [ { diff --git a/frontend/src/app/lightning/node-fee-chart/node-fee-chart.component.ts b/frontend/src/app/lightning/node-fee-chart/node-fee-chart.component.ts index aa0837fce..65a12cf38 100644 --- a/frontend/src/app/lightning/node-fee-chart/node-fee-chart.component.ts +++ b/frontend/src/app/lightning/node-fee-chart/node-fee-chart.component.ts @@ -1,11 +1,11 @@ import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core'; -import { EChartsOption } from '../../graphs/echarts'; +import { EChartsOption } from '@app/graphs/echarts'; import { switchMap } from 'rxjs/operators'; -import { download } from '../../shared/graphs.utils'; -import { LightningApiService } from '../lightning-api.service'; +import { download } from '@app/shared/graphs.utils'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; import { ActivatedRoute, ParamMap } from '@angular/router'; -import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; -import { StateService } from '../../services/state.service'; +import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-node-fee-chart', diff --git a/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts index 91813400d..de9711d01 100644 --- a/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts +++ b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts @@ -1,14 +1,14 @@ import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core'; -import { EChartsOption } from '../../graphs/echarts'; +import { EChartsOption } from '@app/graphs/echarts'; import { Observable } from 'rxjs'; import { switchMap, tap } from 'rxjs/operators'; import { formatNumber } from '@angular/common'; import { UntypedFormGroup } from '@angular/forms'; -import { StorageService } from '../../services/storage.service'; -import { download } from '../../shared/graphs.utils'; -import { LightningApiService } from '../lightning-api.service'; +import { StorageService } from '@app/services/storage.service'; +import { download } from '@app/shared/graphs.utils'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; import { ActivatedRoute, ParamMap } from '@angular/router'; -import { StateService } from '../../services/state.service'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-node-statistics-chart', diff --git a/frontend/src/app/lightning/node-statistics/node-statistics.component.ts b/frontend/src/app/lightning/node-statistics/node-statistics.component.ts index 338e17ab8..a4b061e38 100644 --- a/frontend/src/app/lightning/node-statistics/node-statistics.component.ts +++ b/frontend/src/app/lightning/node-statistics/node-statistics.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; -import { INodesStatistics } from '../../interfaces/node-api.interface'; +import { INodesStatistics } from '@interfaces/node-api.interface'; @Component({ selector: 'app-node-statistics', diff --git a/frontend/src/app/lightning/node/node-preview.component.ts b/frontend/src/app/lightning/node/node-preview.component.ts index d47a8c5ad..259313de6 100644 --- a/frontend/src/app/lightning/node/node-preview.component.ts +++ b/frontend/src/app/lightning/node/node-preview.component.ts @@ -2,11 +2,11 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { Observable } from 'rxjs'; import { catchError, map, switchMap } from 'rxjs/operators'; -import { SeoService } from '../../services/seo.service'; -import { OpenGraphService } from '../../services/opengraph.service'; -import { getFlagEmoji } from '../../shared/common.utils'; -import { LightningApiService } from '../lightning-api.service'; -import { isMobile } from '../../shared/common.utils'; +import { SeoService } from '@app/services/seo.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; +import { getFlagEmoji } from '@app/shared/common.utils'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; +import { isMobile } from '@app/shared/common.utils'; @Component({ selector: 'app-node-preview', diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index cc7c8e5b1..4ad455dee 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -2,13 +2,13 @@ import { ChangeDetectionStrategy, Component, OnInit, ChangeDetectorRef } from '@ import { ActivatedRoute, ParamMap } from '@angular/router'; import { Observable, of } from 'rxjs'; import { catchError, map, switchMap, tap, share } from 'rxjs/operators'; -import { SeoService } from '../../services/seo.service'; -import { ApiService } from '../../services/api.service'; -import { LightningApiService } from '../lightning-api.service'; -import { GeolocationData } from '../../shared/components/geolocation/geolocation.component'; -import { ILiquidityAd, parseLiquidityAdHex } from './liquidity-ad'; -import { haversineDistance, kmToMiles } from '../../../app/shared/common.utils'; -import { ServicesApiServices } from '../../services/services-api.service'; +import { SeoService } from '@app/services/seo.service'; +import { ApiService } from '@app/services/api.service'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; +import { GeolocationData } from '@app/shared/components/geolocation/geolocation.component'; +import { ILiquidityAd, parseLiquidityAdHex } from '@app/lightning/node/liquidity-ad'; +import { haversineDistance, kmToMiles } from '@app/shared/common.utils'; +import { ServicesApiServices } from '@app/services/services-api.service'; interface CustomRecord { type: string; diff --git a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts index 3447348be..a1efdc144 100644 --- a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts +++ b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts @@ -1,16 +1,16 @@ import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter, NgZone, OnInit } from '@angular/core'; -import { SeoService } from '../../services/seo.service'; -import { ApiService } from '../../services/api.service'; +import { SeoService } from '@app/services/seo.service'; +import { ApiService } from '@app/services/api.service'; import { delay, Observable, of, switchMap, tap, zip } from 'rxjs'; -import { AssetsService } from '../../services/assets.service'; +import { AssetsService } from '@app/services/assets.service'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; -import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; -import { StateService } from '../../services/state.service'; -import { EChartsOption, echarts } from '../../graphs/echarts'; -import { isMobile } from '../../shared/common.utils'; -import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; -import { getFlagEmoji } from '../../shared/common.utils'; -import { lerpColor } from '../../shared/graphs.utils'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; +import { StateService } from '@app/services/state.service'; +import { EChartsOption, echarts } from '@app/graphs/echarts'; +import { isMobile } from '@app/shared/common.utils'; +import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe'; +import { getFlagEmoji } from '@app/shared/common.utils'; +import { lerpColor } from '@app/shared/graphs.utils'; @Component({ selector: 'app-nodes-channels-map', diff --git a/frontend/src/app/lightning/nodes-channels/node-channels.component.ts b/frontend/src/app/lightning/nodes-channels/node-channels.component.ts index fce014e77..96f74bfed 100644 --- a/frontend/src/app/lightning/nodes-channels/node-channels.component.ts +++ b/frontend/src/app/lightning/nodes-channels/node-channels.component.ts @@ -1,13 +1,13 @@ import { formatNumber } from '@angular/common'; import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges } from '@angular/core'; import { Router } from '@angular/router'; -import { EChartsOption, TreemapSeriesOption } from '../../graphs/echarts'; +import { EChartsOption, TreemapSeriesOption } from '@app/graphs/echarts'; import { Observable, share, switchMap, tap } from 'rxjs'; -import { lerpColor } from '../../shared/graphs.utils'; -import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; -import { LightningApiService } from '../lightning-api.service'; -import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; -import { StateService } from '../../services/state.service'; +import { lerpColor } from '@app/shared/graphs.utils'; +import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-node-channels', @@ -144,4 +144,4 @@ export class NodeChannels implements OnChanges { }); }); } -} \ No newline at end of file +} diff --git a/frontend/src/app/lightning/nodes-map/nodes-map.component.ts b/frontend/src/app/lightning/nodes-map/nodes-map.component.ts index 50df2f986..c4d56676c 100644 --- a/frontend/src/app/lightning/nodes-map/nodes-map.component.ts +++ b/frontend/src/app/lightning/nodes-map/nodes-map.component.ts @@ -1,15 +1,15 @@ import { ChangeDetectionStrategy, Component, Inject, Input, Output, EventEmitter, LOCALE_ID, NgZone, OnDestroy, OnInit, OnChanges } from '@angular/core'; -import { SeoService } from '../../services/seo.service'; -import { ApiService } from '../../services/api.service'; +import { SeoService } from '@app/services/seo.service'; +import { ApiService } from '@app/services/api.service'; import { Observable, BehaviorSubject, switchMap, tap, combineLatest } from 'rxjs'; -import { AssetsService } from '../../services/assets.service'; -import { EChartsOption, echarts } from '../../graphs/echarts'; -import { lerpColor } from '../../shared/graphs.utils'; +import { AssetsService } from '@app/services/assets.service'; +import { EChartsOption, echarts } from '@app/graphs/echarts'; +import { lerpColor } from '@app/shared/graphs.utils'; import { Router } from '@angular/router'; -import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; -import { StateService } from '../../services/state.service'; -import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; -import { getFlagEmoji } from '../../shared/common.utils'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; +import { StateService } from '@app/services/state.service'; +import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe'; +import { getFlagEmoji } from '@app/shared/common.utils'; @Component({ selector: 'app-nodes-map', diff --git a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts index d947bb327..a5d0d076a 100644 --- a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts +++ b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts @@ -1,17 +1,17 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding, OnChanges, SimpleChanges } from '@angular/core'; -import { echarts, EChartsOption, LineSeriesOption } from '../../graphs/echarts'; +import { echarts, EChartsOption, LineSeriesOption } from '@app/graphs/echarts'; import { Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; import { formatNumber } from '@angular/common'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { StorageService } from '../../services/storage.service'; -import { MiningService } from '../../services/mining.service'; -import { download } from '../../shared/graphs.utils'; -import { SeoService } from '../../services/seo.service'; -import { LightningApiService } from '../lightning-api.service'; -import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; -import { isMobile } from '../../shared/common.utils'; -import { StateService } from '../../services/state.service'; +import { StorageService } from '@app/services/storage.service'; +import { MiningService } from '@app/services/mining.service'; +import { download } from '@app/shared/graphs.utils'; +import { SeoService } from '@app/services/seo.service'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; +import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe'; +import { isMobile } from '@app/shared/common.utils'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-nodes-networks-chart', diff --git a/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts b/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts index 7eba4f9b7..986484d8a 100644 --- a/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts +++ b/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts @@ -1,15 +1,15 @@ import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone } from '@angular/core'; import { Router } from '@angular/router'; -import { EChartsOption, PieSeriesOption } from '../../graphs/echarts'; +import { EChartsOption, PieSeriesOption } from '@app/graphs/echarts'; import { map, Observable, share, tap } from 'rxjs'; -import { chartColors } from '../../app.constants'; -import { ApiService } from '../../services/api.service'; -import { SeoService } from '../../services/seo.service'; -import { StateService } from '../../services/state.service'; -import { download } from '../../shared/graphs.utils'; -import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; -import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; -import { getFlagEmoji } from '../../shared/common.utils'; +import { chartColors } from '@app/app.constants'; +import { ApiService } from '@app/services/api.service'; +import { SeoService } from '@app/services/seo.service'; +import { StateService } from '@app/services/state.service'; +import { download } from '@app/shared/graphs.utils'; +import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; +import { getFlagEmoji } from '@app/shared/common.utils'; @Component({ selector: 'app-nodes-per-country-chart', diff --git a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts index 619ee01c0..b2b9e1f5c 100644 --- a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts +++ b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts @@ -1,10 +1,10 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { BehaviorSubject, combineLatest, map, Observable, share, tap } from 'rxjs'; -import { ApiService } from '../../services/api.service'; -import { SeoService } from '../../services/seo.service'; -import { getFlagEmoji } from '../../shared/common.utils'; -import { GeolocationData } from '../../shared/components/geolocation/geolocation.component'; +import { ApiService } from '@app/services/api.service'; +import { SeoService } from '@app/services/seo.service'; +import { getFlagEmoji } from '@app/shared/common.utils'; +import { GeolocationData } from '@app/shared/components/geolocation/geolocation.component'; @Component({ selector: 'app-nodes-per-country', diff --git a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts index 429d7ee36..56a03a524 100644 --- a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts +++ b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts @@ -1,15 +1,15 @@ import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { EChartsOption, PieSeriesOption } from '../../graphs/echarts'; +import { EChartsOption, PieSeriesOption } from '@app/graphs/echarts'; import { combineLatest, map, Observable, share, startWith, Subject, switchMap, tap } from 'rxjs'; -import { chartColors } from '../../app.constants'; -import { ApiService } from '../../services/api.service'; -import { SeoService } from '../../services/seo.service'; -import { StateService } from '../../services/state.service'; -import { isMobile } from '../../shared/common.utils'; -import { download } from '../../shared/graphs.utils'; -import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; -import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; +import { chartColors } from '@app/app.constants'; +import { ApiService } from '@app/services/api.service'; +import { SeoService } from '@app/services/seo.service'; +import { StateService } from '@app/services/state.service'; +import { isMobile } from '@app/shared/common.utils'; +import { download } from '@app/shared/graphs.utils'; +import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; @Component({ selector: 'app-nodes-per-isp-chart', diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts index 313353ab8..9fc071eb5 100644 --- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts +++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts @@ -1,11 +1,11 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { catchError, map, switchMap, Observable, share, of } from 'rxjs'; -import { ApiService } from '../../services/api.service'; -import { SeoService } from '../../services/seo.service'; -import { OpenGraphService } from '../../services/opengraph.service'; -import { getFlagEmoji } from '../../shared/common.utils'; -import { GeolocationData } from '../../shared/components/geolocation/geolocation.component'; +import { ApiService } from '@app/services/api.service'; +import { SeoService } from '@app/services/seo.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; +import { getFlagEmoji } from '@app/shared/common.utils'; +import { GeolocationData } from '@app/shared/components/geolocation/geolocation.component'; @Component({ selector: 'app-nodes-per-isp-preview', diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts index f6c61a9f6..1c8ad3e1b 100644 --- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts +++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts @@ -1,10 +1,10 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { BehaviorSubject, combineLatest, map, Observable, share, tap } from 'rxjs'; -import { ApiService } from '../../services/api.service'; -import { SeoService } from '../../services/seo.service'; -import { getFlagEmoji } from '../../shared/common.utils'; -import { GeolocationData } from '../../shared/components/geolocation/geolocation.component'; +import { ApiService } from '@app/services/api.service'; +import { SeoService } from '@app/services/seo.service'; +import { getFlagEmoji } from '@app/shared/common.utils'; +import { GeolocationData } from '@app/shared/components/geolocation/geolocation.component'; @Component({ selector: 'app-nodes-per-isp', diff --git a/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.ts b/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.ts index 8a1eae3dc..569bd8827 100644 --- a/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { LightningApiService } from '../lightning-api.service'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; import { share } from 'rxjs/operators'; import { Observable } from 'rxjs'; -import { INodesStatistics } from '../../interfaces/node-api.interface'; +import { INodesStatistics } from '@interfaces/node-api.interface'; @Component({ selector: 'app-nodes-ranking', diff --git a/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts index d83f3db0a..a554341b9 100644 --- a/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { map, Observable } from 'rxjs'; -import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component'; -import { SeoService } from '../../../services/seo.service'; -import { IOldestNodes } from '../../../interfaces/node-api.interface'; -import { LightningApiService } from '../../lightning-api.service'; +import { GeolocationData } from '@app/shared/components/geolocation/geolocation.component'; +import { SeoService } from '@app/services/seo.service'; +import { IOldestNodes } from '@interfaces/node-api.interface'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; @Component({ selector: 'app-oldest-nodes', diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts index 0b8c03bbd..24c8757b6 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts @@ -1,10 +1,10 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { combineLatest, map, Observable } from 'rxjs'; -import { INodesRanking, INodesStatistics, ITopNodesPerCapacity } from '../../../interfaces/node-api.interface'; -import { SeoService } from '../../../services/seo.service'; -import { StateService } from '../../../services/state.service'; -import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component'; -import { LightningApiService } from '../../lightning-api.service'; +import { INodesRanking, INodesStatistics, ITopNodesPerCapacity } from '@interfaces/node-api.interface'; +import { SeoService } from '@app/services/seo.service'; +import { StateService } from '@app/services/state.service'; +import { GeolocationData } from '@app/shared/components/geolocation/geolocation.component'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; @Component({ selector: 'app-top-nodes-per-capacity', diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts index 56d55a5d3..6dbcb9c3e 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts @@ -1,10 +1,10 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { combineLatest, map, Observable } from 'rxjs'; -import { INodesRanking, INodesStatistics, ITopNodesPerChannels } from '../../../interfaces/node-api.interface'; -import { SeoService } from '../../../services/seo.service'; -import { StateService } from '../../../services/state.service'; -import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component'; -import { LightningApiService } from '../../lightning-api.service'; +import { INodesRanking, INodesStatistics, ITopNodesPerChannels } from '@interfaces/node-api.interface'; +import { SeoService } from '@app/services/seo.service'; +import { StateService } from '@app/services/state.service'; +import { GeolocationData } from '@app/shared/components/geolocation/geolocation.component'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; @Component({ selector: 'app-top-nodes-per-channels', diff --git a/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts b/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts index 178ca783c..5561d086e 100644 --- a/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts +++ b/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts @@ -1,8 +1,8 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { Observable, share } from 'rxjs'; -import { INodesRanking } from '../../interfaces/node-api.interface'; -import { SeoService } from '../../services/seo.service'; -import { LightningApiService } from '../lightning-api.service'; +import { INodesRanking } from '@interfaces/node-api.interface'; +import { SeoService } from '@app/services/seo.service'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; @Component({ selector: 'app-nodes-rankings-dashboard', diff --git a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts index 77ce42f26..08523088c 100644 --- a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts +++ b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts @@ -1,17 +1,17 @@ import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding, OnChanges, SimpleChanges } from '@angular/core'; -import { echarts, EChartsOption } from '../../graphs/echarts'; +import { echarts, EChartsOption } from '@app/graphs/echarts'; import { Observable, combineLatest, fromEvent } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { SeoService } from '../../services/seo.service'; +import { SeoService } from '@app/services/seo.service'; import { formatNumber } from '@angular/common'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { StorageService } from '../../services/storage.service'; -import { MiningService } from '../../services/mining.service'; -import { download } from '../../shared/graphs.utils'; -import { LightningApiService } from '../lightning-api.service'; -import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; -import { isMobile } from '../../shared/common.utils'; -import { StateService } from '../../services/state.service'; +import { StorageService } from '@app/services/storage.service'; +import { MiningService } from '@app/services/mining.service'; +import { download } from '@app/shared/graphs.utils'; +import { LightningApiService } from '@app/lightning/lightning-api.service'; +import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe'; +import { isMobile } from '@app/shared/common.utils'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-lightning-statistics-chart', diff --git a/frontend/src/app/liquid/liquid-graphs.module.ts b/frontend/src/app/liquid/liquid-graphs.module.ts index 3da93fc9d..9cfd7da83 100644 --- a/frontend/src/app/liquid/liquid-graphs.module.ts +++ b/frontend/src/app/liquid/liquid-graphs.module.ts @@ -1,7 +1,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Routes, RouterModule } from '@angular/router'; -import { LiquidMasterPageComponent } from '../components/liquid-master-page/liquid-master-page.component'; +import { LiquidMasterPageComponent } from '@components/liquid-master-page/liquid-master-page.component'; const routes: Routes = [ { diff --git a/frontend/src/app/liquid/liquid-master-page.module.ts b/frontend/src/app/liquid/liquid-master-page.module.ts index 5d5f0b8dd..17c2c8c41 100644 --- a/frontend/src/app/liquid/liquid-master-page.module.ts +++ b/frontend/src/app/liquid/liquid-master-page.module.ts @@ -1,25 +1,25 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Routes, RouterModule } from '@angular/router'; -import { SharedModule } from '../shared/shared.module'; +import { SharedModule } from '@app/shared/shared.module'; import { NgxEchartsModule } from 'ngx-echarts'; -import { LiquidMasterPageComponent } from '../components/liquid-master-page/liquid-master-page.component'; +import { LiquidMasterPageComponent } from '@components/liquid-master-page/liquid-master-page.component'; -import { StartComponent } from '../components/start/start.component'; -import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component'; -import { BlocksList } from '../components/blocks-list/blocks-list.component'; -import { AssetGroupComponent } from '../components/assets/asset-group/asset-group.component'; -import { AssetsComponent } from '../components/assets/assets.component'; -import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component' -import { AssetComponent } from '../components/asset/asset.component'; -import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component'; -import { RecentPegsListComponent } from '../components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component'; -import { FederationWalletComponent } from '../components/liquid-reserves-audit/federation-wallet/federation-wallet.component'; -import { FederationUtxosListComponent } from '../components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component'; -import { FederationAddressesListComponent } from '../components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component'; -import { ServerHealthComponent } from '../components/server-health/server-health.component'; -import { ServerStatusComponent } from '../components/server-health/server-status.component'; +import { StartComponent } from '@components/start/start.component'; +import { PushTransactionComponent } from '@components/push-transaction/push-transaction.component'; +import { BlocksList } from '@components/blocks-list/blocks-list.component'; +import { AssetGroupComponent } from '@components/assets/asset-group/asset-group.component'; +import { AssetsComponent } from '@components/assets/assets.component'; +import { AssetsFeaturedComponent } from '@components/assets/assets-featured/assets-featured.component' +import { AssetComponent } from '@components/asset/asset.component'; +import { AssetsNavComponent } from '@components/assets/assets-nav/assets-nav.component'; +import { RecentPegsListComponent } from '@components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component'; +import { FederationWalletComponent } from '@components/liquid-reserves-audit/federation-wallet/federation-wallet.component'; +import { FederationUtxosListComponent } from '@components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component'; +import { FederationAddressesListComponent } from '@components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component'; +import { ServerHealthComponent } from '@components/server-health/server-health.component'; +import { ServerStatusComponent } from '@components/server-health/server-status.component'; const routes: Routes = [ { @@ -32,7 +32,7 @@ const routes: Routes = [ }, { path: 'about', - loadChildren: () => import('../components/about/about.module').then(m => m.AboutModule), + loadChildren: () => import('@components/about/about.module').then(m => m.AboutModule), }, { path: 'blocks/:page', @@ -44,27 +44,27 @@ const routes: Routes = [ }, { path: 'terms-of-service', - loadChildren: () => import('../components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule), + loadChildren: () => import('@components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule), }, { path: 'privacy-policy', - loadChildren: () => import('../components/privacy-policy/privacy-policy.module').then(m => m.PrivacyPolicyModule), + loadChildren: () => import('@components/privacy-policy/privacy-policy.module').then(m => m.PrivacyPolicyModule), }, { path: 'trademark-policy', - loadChildren: () => import('../components/trademark-policy/trademark-policy.module').then(m => m.TrademarkModule), + loadChildren: () => import('@components/trademark-policy/trademark-policy.module').then(m => m.TrademarkModule), }, { path: 'tx', component: StartComponent, data: { preload: true, networkSpecific: true }, - loadChildren: () => import('../components/transaction/transaction.module').then(m => m.TransactionModule), + loadChildren: () => import('@components/transaction/transaction.module').then(m => m.TransactionModule), }, { path: 'block', component: StartComponent, data: { preload: true, networkSpecific: true }, - loadChildren: () => import('../components/block/block.module').then(m => m.BlockModule), + loadChildren: () => import('@components/block/block.module').then(m => m.BlockModule), }, { path: 'audit/wallet', @@ -178,4 +178,4 @@ export class LiquidRoutingModule { } FederationUtxosListComponent, ] }) -export class LiquidMasterPageModule { } \ No newline at end of file +export class LiquidMasterPageModule { } diff --git a/frontend/src/app/master-page.module.ts b/frontend/src/app/master-page.module.ts index 510756cbc..2ee2e0bd8 100644 --- a/frontend/src/app/master-page.module.ts +++ b/frontend/src/app/master-page.module.ts @@ -1,18 +1,18 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Routes, RouterModule, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; -import { MasterPageComponent } from './components/master-page/master-page.component'; -import { SharedModule } from './shared/shared.module'; +import { MasterPageComponent } from '@components/master-page/master-page.component'; +import { SharedModule } from '@app/shared/shared.module'; -import { StartComponent } from './components/start/start.component'; -import { PushTransactionComponent } from './components/push-transaction/push-transaction.component'; -import { TestTransactionsComponent } from './components/test-transactions/test-transactions.component'; -import { CalculatorComponent } from './components/calculator/calculator.component'; -import { BlocksList } from './components/blocks-list/blocks-list.component'; -import { RbfList } from './components/rbf-list/rbf-list.component'; -import { ServerHealthComponent } from './components/server-health/server-health.component'; -import { ServerStatusComponent } from './components/server-health/server-status.component'; -import { FaucetComponent } from './components/faucet/faucet.component' +import { StartComponent } from '@components/start/start.component'; +import { PushTransactionComponent } from '@components/push-transaction/push-transaction.component'; +import { TestTransactionsComponent } from '@components/test-transactions/test-transactions.component'; +import { CalculatorComponent } from '@components/calculator/calculator.component'; +import { BlocksList } from '@components/blocks-list/blocks-list.component'; +import { RbfList } from '@components/rbf-list/rbf-list.component'; +import { ServerHealthComponent } from '@components/server-health/server-health.component'; +import { ServerStatusComponent } from '@components/server-health/server-status.component'; +import { FaucetComponent } from '@components/faucet/faucet.component' const browserWindow = window || {}; // @ts-ignore @@ -42,7 +42,7 @@ const routes: Routes = [ }, { path: 'about', - loadChildren: () => import('./components/about/about.module').then(m => m.AboutModule), + loadChildren: () => import('@components/about/about.module').then(m => m.AboutModule), }, { path: 'blocks/:page', @@ -58,40 +58,40 @@ const routes: Routes = [ }, { path: 'terms-of-service', - loadChildren: () => import('./components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule), + loadChildren: () => import('@components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule), }, { path: 'privacy-policy', - loadChildren: () => import('./components/privacy-policy/privacy-policy.module').then(m => m.PrivacyPolicyModule), + loadChildren: () => import('@components/privacy-policy/privacy-policy.module').then(m => m.PrivacyPolicyModule), }, { path: 'trademark-policy', - loadChildren: () => import('./components/trademark-policy/trademark-policy.module').then(m => m.TrademarkModule), + loadChildren: () => import('@components/trademark-policy/trademark-policy.module').then(m => m.TrademarkModule), }, { path: 'tx', component: StartComponent, data: { preload: true, networkSpecific: true }, - loadChildren: () => import('./components/transaction/transaction.module').then(m => m.TransactionModule), + loadChildren: () => import('@components/transaction/transaction.module').then(m => m.TransactionModule), }, { path: 'block', component: StartComponent, data: { preload: true, networkSpecific: true }, - loadChildren: () => import('./components/block/block.module').then(m => m.BlockModule), + loadChildren: () => import('@components/block/block.module').then(m => m.BlockModule), }, { path: 'docs', - loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule), + loadChildren: () => import('@app/docs/docs.module').then(m => m.DocsModule), data: { preload: true }, }, { path: 'api', - loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) + loadChildren: () => import('@app/docs/docs.module').then(m => m.DocsModule) }, { path: 'lightning', - loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule), + loadChildren: () => import('@app/lightning/lightning.module').then(m => m.LightningModule), data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true, networks: ['bitcoin'] }, }, { diff --git a/frontend/src/app/previews.module.ts b/frontend/src/app/previews.module.ts index 95124f232..abfdced59 100644 --- a/frontend/src/app/previews.module.ts +++ b/frontend/src/app/previews.module.ts @@ -1,15 +1,15 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { SharedModule } from './shared/shared.module'; +import { SharedModule } from '@app/shared/shared.module'; import { RouterModule } from '@angular/router'; -import { GraphsModule } from './graphs/graphs.module'; +import { GraphsModule } from '@app/graphs/graphs.module'; import { PreviewsRoutingModule } from './previews.routing.module'; -import { TransactionPreviewComponent } from './components/transaction/transaction-preview.component'; -import { BlockPreviewComponent } from './components/block/block-preview.component'; -import { AddressPreviewComponent } from './components/address/address-preview.component'; -import { PoolPreviewComponent } from './components/pool/pool-preview.component'; -import { MasterPagePreviewComponent } from './components/master-page-preview/master-page-preview.component'; -import { TxBowtieModule } from './components/tx-bowtie-graph/tx-bowtie.module'; +import { TransactionPreviewComponent } from '@components/transaction/transaction-preview.component'; +import { BlockPreviewComponent } from '@components/block/block-preview.component'; +import { AddressPreviewComponent } from '@components/address/address-preview.component'; +import { PoolPreviewComponent } from '@components/pool/pool-preview.component'; +import { MasterPagePreviewComponent } from '@components/master-page-preview/master-page-preview.component'; +import { TxBowtieModule } from '@components/tx-bowtie-graph/tx-bowtie.module'; @NgModule({ declarations: [ TransactionPreviewComponent, diff --git a/frontend/src/app/previews.routing.module.ts b/frontend/src/app/previews.routing.module.ts index 6ac44a370..92ea113b8 100644 --- a/frontend/src/app/previews.routing.module.ts +++ b/frontend/src/app/previews.routing.module.ts @@ -1,10 +1,10 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { TransactionPreviewComponent } from './components/transaction/transaction-preview.component'; -import { BlockPreviewComponent } from './components/block/block-preview.component'; -import { AddressPreviewComponent } from './components/address/address-preview.component'; -import { PoolPreviewComponent } from './components/pool/pool-preview.component'; -import { MasterPagePreviewComponent } from './components/master-page-preview/master-page-preview.component'; +import { TransactionPreviewComponent } from '@components/transaction/transaction-preview.component'; +import { BlockPreviewComponent } from '@components/block/block-preview.component'; +import { AddressPreviewComponent } from '@components/address/address-preview.component'; +import { PoolPreviewComponent } from '@components/pool/pool-preview.component'; +import { MasterPagePreviewComponent } from '@components/master-page-preview/master-page-preview.component'; const routes: Routes = [ { @@ -31,7 +31,7 @@ const routes: Routes = [ }, { path: 'lightning', - loadChildren: () => import('./lightning/lightning-previews.module').then(m => m.LightningPreviewsModule), + loadChildren: () => import('@app/lightning/lightning-previews.module').then(m => m.LightningPreviewsModule), data: { preload: true }, }, ], diff --git a/frontend/src/app/route-guards.ts b/frontend/src/app/route-guards.ts index 4808713c1..780e997db 100644 --- a/frontend/src/app/route-guards.ts +++ b/frontend/src/app/route-guards.ts @@ -1,6 +1,6 @@ import { Injectable, inject } from '@angular/core'; import { CanMatchFn, Route, Router, UrlSegment } from '@angular/router'; -import { NavigationService } from './services/navigation.service'; +import { NavigationService } from '@app/services/navigation.service'; @Injectable({ providedIn: 'root' @@ -13,10 +13,11 @@ class GuardService { trackerGuard(route: Route, segments: UrlSegment[]): boolean { const preferredRoute = this.router.getCurrentNavigation()?.extractedUrl.queryParams?.mode; - return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98; + const path = this.router.getCurrentNavigation()?.extractedUrl.root.children.primary.segments; + return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98 && !(path.length === 2 && ['push', 'test'].includes(path[1].path)); } } export const TrackerGuard: CanMatchFn = (route: Route, segments: UrlSegment[]): boolean => { return inject(GuardService).trackerGuard(route, segments); -}; \ No newline at end of file +}; diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index fa52ec707..08251ddae 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -1,14 +1,14 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, - RbfTree, BlockAudit, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo, RecentPeg, PegsVolume, AccelerationInfo, TestMempoolAcceptResult } from '../interfaces/node-api.interface'; + RbfTree, BlockAudit, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo, RecentPeg, PegsVolume, AccelerationInfo, TestMempoolAcceptResult, WalletAddress, SubmitPackageResult } from '../interfaces/node-api.interface'; import { BehaviorSubject, Observable, catchError, filter, map, of, shareReplay, take, tap } from 'rxjs'; -import { StateService } from './state.service'; -import { Transaction } from '../interfaces/electrs.interface'; -import { Conversion } from './price.service'; -import { StorageService } from './storage.service'; -import { WebsocketResponse } from '../interfaces/websocket.interface'; -import { TxAuditStatus } from '../components/transaction/transaction.component'; +import { StateService } from '@app/services/state.service'; +import { Transaction } from '@interfaces/electrs.interface'; +import { Conversion } from '@app/services/price.service'; +import { StorageService } from '@app/services/storage.service'; +import { WebsocketResponse } from '@interfaces/websocket.interface'; +import { TxAuditStatus } from '@components/transaction/transaction.component'; @Injectable({ providedIn: 'root' @@ -244,6 +244,19 @@ export class ApiService { return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + `/api/txs/test${maxfeerate != null ? '?maxfeerate=' + maxfeerate.toFixed(8) : ''}`, rawTxs); } + submitPackage$(rawTxs: string[], maxfeerate?: number, maxburnamount?: number): Observable { + const queryParams = []; + + if (maxfeerate) { + queryParams.push(`maxfeerate=${maxfeerate}`); + } + + if (maxburnamount) { + queryParams.push(`maxburnamount=${maxburnamount}`); + } + return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/v1/txs/package' + (queryParams.length > 0 ? `?${queryParams.join('&')}` : ''), rawTxs); + } + getTransactionStatus$(txid: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + txid + '/status'); } @@ -504,6 +517,12 @@ export class ApiService { ); } + getWallet$(walletName: string): Observable> { + return this.httpClient.get>( + this.apiBaseUrl + this.apiBasePath + `/api/v1/wallet/${walletName}` + ); + } + getAccelerationsByPool$(slug: string): Observable { return this.httpClient.get( this.apiBaseUrl + this.apiBasePath + `/api/v1/accelerations/pool/${slug}` diff --git a/frontend/src/app/services/assets.service.ts b/frontend/src/app/services/assets.service.ts index 9c6b5dba0..bb8756bd7 100644 --- a/frontend/src/app/services/assets.service.ts +++ b/frontend/src/app/services/assets.service.ts @@ -2,9 +2,9 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map, shareReplay, switchMap } from 'rxjs/operators'; -import { StateService } from './state.service'; -import { environment } from '../../../src/environments/environment'; -import { AssetExtended } from '../interfaces/electrs.interface'; +import { StateService } from '@app/services/state.service'; +import { environment } from '@environments/environment'; +import { AssetExtended } from '@interfaces/electrs.interface'; @Injectable({ providedIn: 'root' diff --git a/frontend/src/app/services/auth.service.ts b/frontend/src/app/services/auth.service.ts index 100ddbdc4..db910779e 100644 --- a/frontend/src/app/services/auth.service.ts +++ b/frontend/src/app/services/auth.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { catchError, map, Observable, of, ReplaySubject, switchMap, tap } from 'rxjs'; -import { ServicesApiServices } from './services-api.service'; +import { ServicesApiServices } from '@app/services/services-api.service'; export interface IAuth { token: string; diff --git a/frontend/src/app/services/cache.service.ts b/frontend/src/app/services/cache.service.ts index f15154b46..246008043 100644 --- a/frontend/src/app/services/cache.service.ts +++ b/frontend/src/app/services/cache.service.ts @@ -1,9 +1,9 @@ import { Injectable } from '@angular/core'; import { firstValueFrom, Subject, Subscription} from 'rxjs'; -import { Transaction } from '../interfaces/electrs.interface'; -import { BlockExtended } from '../interfaces/node-api.interface'; -import { StateService } from './state.service'; -import { ApiService } from './api.service'; +import { Transaction } from '@interfaces/electrs.interface'; +import { BlockExtended } from '@interfaces/node-api.interface'; +import { StateService } from '@app/services/state.service'; +import { ApiService } from '@app/services/api.service'; const BLOCK_CACHE_SIZE = 500; const KEEP_RECENT_BLOCKS = 50; @@ -133,4 +133,4 @@ export class CacheService { getCachedBlock(height) { return this.blockCache[height]; } -} \ No newline at end of file +} diff --git a/frontend/src/app/services/electrs-api.service.ts b/frontend/src/app/services/electrs-api.service.ts index 7faaea87c..3cd5b5abd 100644 --- a/frontend/src/app/services/electrs-api.service.ts +++ b/frontend/src/app/services/electrs-api.service.ts @@ -1,10 +1,10 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { BehaviorSubject, Observable, catchError, filter, from, of, shareReplay, switchMap, take, tap } from 'rxjs'; -import { Transaction, Address, Outspend, Recent, Asset, ScriptHash, AddressTxSummary } from '../interfaces/electrs.interface'; -import { StateService } from './state.service'; -import { BlockExtended } from '../interfaces/node-api.interface'; -import { calcScriptHash$ } from '../bitcoin.utils'; +import { Transaction, Address, Outspend, Recent, Asset, ScriptHash, AddressTxSummary, Utxo } from '../interfaces/electrs.interface'; +import { StateService } from '@app/services/state.service'; +import { BlockExtended } from '@interfaces/node-api.interface'; +import { calcScriptHash$ } from '@app/bitcoin.utils'; @Injectable({ providedIn: 'root' @@ -107,6 +107,10 @@ export class ElectrsApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/block-height/' + height, {responseType: 'text'}); } + getBlockTxId$(hash: string, index: number): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/block/' + hash + '/txid/' + index, { responseType: 'text' }); + } + getAddress$(address: string): Observable
{ return this.httpClient.get
(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address); } @@ -138,6 +142,14 @@ export class ElectrsApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params }); } + getAddressesTransactions$(addresses: string[], txid?: string): Observable { + let params = new HttpParams(); + if (txid) { + params = params.append('after_txid', txid); + } + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/addresses/txs?addresses=${addresses.join(',')}`, { params }); + } + getAddressSummary$(address: string, txid?: string): Observable { let params = new HttpParams(); if (txid) { @@ -146,6 +158,14 @@ export class ElectrsApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs/summary', { params }); } + getAddressesSummary$(addresses: string[], txid?: string): Observable { + let params = new HttpParams(); + if (txid) { + params = params.append('after_txid', txid); + } + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/addresses/txs/summary?addresses=${addresses.join(',')}`, { params }); + } + getScriptHashTransactions$(script: string, txid?: string): Observable { let params = new HttpParams(); if (txid) { @@ -156,6 +176,16 @@ export class ElectrsApiService { ); } + getScriptHashesTransactions$(scripts: string[], txid?: string): Observable { + let params = new HttpParams(); + if (txid) { + params = params.append('after_txid', txid); + } + return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe( + switchMap(scriptHashes => this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/scripthashes/txs?scripthashes=${scriptHashes.join(',')}`, { params })), + ); + } + getScriptHashSummary$(script: string, txid?: string): Observable { let params = new HttpParams(); if (txid) { @@ -166,6 +196,26 @@ export class ElectrsApiService { ); } + getAddressUtxos$(address: string): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/utxo'); + } + + getScriptHashUtxos$(script: string): Observable { + return from(calcScriptHash$(script)).pipe( + switchMap(scriptHash => this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash + '/utxo')), + ); + } + + getScriptHashesSummary$(scripts: string[], txid?: string): Observable { + let params = new HttpParams(); + if (txid) { + params = params.append('after_txid', txid); + } + return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe( + switchMap(scriptHashes => this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/scripthashes/txs/summary?scripthashes=${scriptHashes.join(',')}`, { params })), + ); + } + getAsset$(assetId: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId); } diff --git a/frontend/src/app/services/enterprise.service.ts b/frontend/src/app/services/enterprise.service.ts index 4ea890f1f..d7695b94f 100644 --- a/frontend/src/app/services/enterprise.service.ts +++ b/frontend/src/app/services/enterprise.service.ts @@ -1,8 +1,8 @@ import { DOCUMENT } from '@angular/common'; import { Inject, Injectable } from '@angular/core'; -import { ApiService } from './api.service'; -import { SeoService } from './seo.service'; -import { StateService } from './state.service'; +import { ApiService } from '@app/services/api.service'; +import { SeoService } from '@app/services/seo.service'; +import { StateService } from '@app/services/state.service'; import { ActivatedRoute } from '@angular/router'; import { BehaviorSubject } from 'rxjs'; diff --git a/frontend/src/app/services/eta.service.ts b/frontend/src/app/services/eta.service.ts index f632c9adb..cf7719327 100644 --- a/frontend/src/app/services/eta.service.ts +++ b/frontend/src/app/services/eta.service.ts @@ -1,11 +1,11 @@ import { Injectable } from '@angular/core'; -import { AccelerationPosition, CpfpInfo, DifficultyAdjustment, MempoolPosition, SinglePoolStats } from '../interfaces/node-api.interface'; -import { StateService } from './state.service'; -import { MempoolBlock } from '../interfaces/websocket.interface'; -import { Transaction } from '../interfaces/electrs.interface'; -import { MiningService, MiningStats } from './mining.service'; -import { getUnacceleratedFeeRate } from '../shared/transaction.utils'; -import { AccelerationEstimate } from '../components/accelerate-checkout/accelerate-checkout.component'; +import { AccelerationPosition, CpfpInfo, DifficultyAdjustment, MempoolPosition, SinglePoolStats } from '@interfaces/node-api.interface'; +import { StateService } from '@app/services/state.service'; +import { MempoolBlock } from '@interfaces/websocket.interface'; +import { Transaction } from '@interfaces/electrs.interface'; +import { MiningService, MiningStats } from '@app/services/mining.service'; +import { getUnacceleratedFeeRate } from '@app/shared/transaction.utils'; +import { AccelerationEstimate } from '@components/accelerate-checkout/accelerate-checkout.component'; import { Observable, combineLatest, map, of, share, shareReplay, tap } from 'rxjs'; export interface ETA { @@ -28,7 +28,7 @@ export class EtaService { return combineLatest([ this.stateService.mempoolTxPosition$.pipe(map(p => p?.position)), this.stateService.difficultyAdjustment$, - miningStats ? of(miningStats) : this.miningService.getMiningStats('1w'), + miningStats ? of(miningStats) : this.miningService.getMiningStats('1m'), ]).pipe( map(([mempoolPosition, da, miningStats]) => { if (!mempoolPosition || !estimate?.pools?.length || !miningStats || !da) { @@ -166,7 +166,7 @@ export class EtaService { pools[pool.poolUniqueId] = pool; } const unacceleratedPosition = this.mempoolPositionFromFees(getUnacceleratedFeeRate(tx, true), mempoolBlocks); - const totalAcceleratedHashrate = accelerationPositions.reduce((total, pos) => total + (pools[pos.poolId].lastEstimatedHashrate), 0); + const totalAcceleratedHashrate = accelerationPositions.reduce((total, pos) => total + (pools[pos.poolId]?.lastEstimatedHashrate || 0), 0); const shares = [ { block: unacceleratedPosition.block, @@ -174,7 +174,7 @@ export class EtaService { }, ...accelerationPositions.map(pos => ({ block: pos.block, - hashrateShare: ((pools[pos.poolId].lastEstimatedHashrate) / miningStats.lastEstimatedHashrate) + hashrateShare: ((pools[pos.poolId]?.lastEstimatedHashrate || 0) / miningStats.lastEstimatedHashrate) })) ]; return this.calculateETAFromShares(shares, da); @@ -204,7 +204,7 @@ export class EtaService { let tailProb = 0; let Q = 0; - for (let i = 0; i < max; i++) { + for (let i = 0; i <= max; i++) { // find H_i const H = shares.reduce((total, share) => total + (share.block <= i ? share.hashrateShare : 0), 0); // find S_i @@ -215,7 +215,7 @@ export class EtaService { tailProb += S; } // at max depth, the transaction is guaranteed to be mined in the next block if it hasn't already - Q += (1-tailProb); + Q += ((max + 1) * (1-tailProb)); const eta = da.timeAvg * Q; // T x Q return { diff --git a/frontend/src/app/services/language.service.ts b/frontend/src/app/services/language.service.ts index 58ba94a00..b0d2f247c 100644 --- a/frontend/src/app/services/language.service.ts +++ b/frontend/src/app/services/language.service.ts @@ -1,6 +1,6 @@ import { DOCUMENT, getLocaleId } from '@angular/common'; import { LOCALE_ID, Inject, Injectable } from '@angular/core'; -import { languages } from '../app.constants'; +import { languages } from '@app/app.constants'; @Injectable({ providedIn: 'root' diff --git a/frontend/src/app/services/mining.service.ts b/frontend/src/app/services/mining.service.ts index 0dffa0b66..87e9374ea 100644 --- a/frontend/src/app/services/mining.service.ts +++ b/frontend/src/app/services/mining.service.ts @@ -1,10 +1,10 @@ import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { map, tap } from 'rxjs/operators'; -import { PoolsStats, SinglePoolStats } from '../interfaces/node-api.interface'; -import { ApiService } from '../services/api.service'; -import { StateService } from './state.service'; -import { StorageService } from './storage.service'; +import { PoolsStats, SinglePoolStats } from '@interfaces/node-api.interface'; +import { ApiService } from '@app/services/api.service'; +import { StateService } from '@app/services/state.service'; +import { StorageService } from '@app/services/storage.service'; export interface MiningUnits { hashrateDivider: number; diff --git a/frontend/src/app/services/navigation.service.ts b/frontend/src/app/services/navigation.service.ts index 2a3215121..c1b32739a 100644 --- a/frontend/src/app/services/navigation.service.ts +++ b/frontend/src/app/services/navigation.service.ts @@ -2,8 +2,8 @@ import { Injectable } from '@angular/core'; import { Router, NavigationEnd, ActivatedRouteSnapshot } from '@angular/router'; import { BehaviorSubject } from 'rxjs'; import { filter, map } from 'rxjs/operators'; -import { StateService } from './state.service'; -import { RelativeUrlPipe } from '../shared/pipes/relative-url/relative-url.pipe'; +import { StateService } from '@app/services/state.service'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; @Injectable({ providedIn: 'root' diff --git a/frontend/src/app/services/opengraph.service.ts b/frontend/src/app/services/opengraph.service.ts index 5e429ed70..e969dd07a 100644 --- a/frontend/src/app/services/opengraph.service.ts +++ b/frontend/src/app/services/opengraph.service.ts @@ -3,8 +3,8 @@ import { Meta } from '@angular/platform-browser'; import { Router, ActivatedRoute, NavigationEnd } from '@angular/router'; import { filter, map, switchMap } from 'rxjs/operators'; import { combineLatest } from 'rxjs'; -import { StateService } from './state.service'; -import { LanguageService } from './language.service'; +import { StateService } from '@app/services/state.service'; +import { LanguageService } from '@app/services/language.service'; @Injectable({ providedIn: 'root' diff --git a/frontend/src/app/services/ord-api.service.ts b/frontend/src/app/services/ord-api.service.ts new file mode 100644 index 000000000..ae0076906 --- /dev/null +++ b/frontend/src/app/services/ord-api.service.ts @@ -0,0 +1,100 @@ +import { Injectable } from '@angular/core'; +import { catchError, forkJoin, map, Observable, of, switchMap, tap } from 'rxjs'; +import { Inscription } from '@app/shared/ord/inscription.utils'; +import { Transaction } from '@interfaces/electrs.interface'; +import { getNextInscriptionMark, hexToBytes, extractInscriptionData } from '@app/shared/ord/inscription.utils'; +import { decipherRunestone, Runestone, Etching, UNCOMMON_GOODS } from '@app/shared/ord/rune.utils'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; + + +@Injectable({ + providedIn: 'root' +}) +export class OrdApiService { + + constructor( + private electrsApiService: ElectrsApiService, + ) { } + + decodeRunestone$(tx: Transaction): Observable<{ runestone: Runestone, runeInfo: { [id: string]: { etching: Etching; txid: string; } } }> { + const runestone = decipherRunestone(tx); + const runeInfo: { [id: string]: { etching: Etching; txid: string; } } = {}; + + if (runestone) { + const runesToFetch: Set = new Set(); + + if (runestone.mint) { + runesToFetch.add(runestone.mint.toString()); + } + + if (runestone.edicts.length) { + runestone.edicts.forEach(edict => { + runesToFetch.add(edict.id.toString()); + }); + } + + if (runesToFetch.size) { + const runeEtchingObservables = Array.from(runesToFetch).map(runeId => this.getEtchingFromRuneId$(runeId)); + + return forkJoin(runeEtchingObservables).pipe( + map((etchings) => { + etchings.forEach((el) => { + if (el) { + runeInfo[el.runeId] = { etching: el.etching, txid: el.txid }; + } + }); + return { runestone: runestone, runeInfo }; + }) + ); + } + return of({ runestone: runestone, runeInfo }); + } else { + return of({ runestone: null, runeInfo: {} }); + } + } + + // Get etching from runeId by looking up the transaction that etched the rune + getEtchingFromRuneId$(runeId: string): Observable<{ runeId: string; etching: Etching; txid: string; }> { + if (runeId === '1:0') { + return of({ runeId, etching: UNCOMMON_GOODS, txid: '0000000000000000000000000000000000000000000000000000000000000000' }); + } else { + const [blockNumber, txIndex] = runeId.split(':'); + return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockNumber)).pipe( + switchMap(blockHash => this.electrsApiService.getBlockTxId$(blockHash, parseInt(txIndex))), + switchMap(txId => this.electrsApiService.getTransaction$(txId)), + switchMap(tx => { + const runestone = decipherRunestone(tx); + if (runestone) { + const etching = runestone.etching; + if (etching) { + return of({ runeId, etching, txid: tx.txid }); + } + } + return of(null); + }), + catchError(() => of(null)) + ); + } + } + + decodeInscriptions(witness: string): Inscription[] | null { + + const inscriptions: Inscription[] = []; + const raw = hexToBytes(witness); + let startPosition = 0; + + while (true) { + const pointer = getNextInscriptionMark(raw, startPosition); + if (pointer === -1) break; + + const inscription = extractInscriptionData(raw, pointer); + if (inscription) { + inscriptions.push(inscription); + } + + startPosition = pointer; + } + + return inscriptions; + } +} diff --git a/frontend/src/app/services/preload.service.ts b/frontend/src/app/services/preload.service.ts index 386d4deb4..2fce9b98c 100644 --- a/frontend/src/app/services/preload.service.ts +++ b/frontend/src/app/services/preload.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { ElectrsApiService } from '../services/electrs-api.service'; import { Subject, debounceTime, switchMap } from 'rxjs'; -import { ApiService } from './api.service'; +import { ApiService } from '@app/services/api.service'; @Injectable({ providedIn: 'root' diff --git a/frontend/src/app/services/price.service.ts b/frontend/src/app/services/price.service.ts index c342796e0..e5a0c86c8 100644 --- a/frontend/src/app/services/price.service.ts +++ b/frontend/src/app/services/price.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { map, Observable, of, share, shareReplay, tap } from 'rxjs'; -import { ApiService } from './api.service'; -import { StateService } from './state.service'; +import { ApiService } from '@app/services/api.service'; +import { StateService } from '@app/services/state.service'; // nodejs backend interfaces export interface ApiPrice { @@ -320,4 +320,4 @@ export class PriceService { return prices; })); } -} \ No newline at end of file +} diff --git a/frontend/src/app/services/seo.service.ts b/frontend/src/app/services/seo.service.ts index af12e8b47..e5ede4db3 100644 --- a/frontend/src/app/services/seo.service.ts +++ b/frontend/src/app/services/seo.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { Title, Meta } from '@angular/platform-browser'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { filter, map, switchMap } from 'rxjs'; -import { StateService } from './state.service'; +import { StateService } from '@app/services/state.service'; @Injectable({ providedIn: 'root' diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index 1366342f7..2b0f884ff 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -1,21 +1,20 @@ import { Router, NavigationStart } from '@angular/router'; import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { StateService } from './state.service'; -import { StorageService } from './storage.service'; -import { MenuGroup } from '../interfaces/services.interface'; -import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMap } from 'rxjs'; -import { IBackendInfo } from '../interfaces/websocket.interface'; -import { Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface'; -import { AccelerationStats } from '../components/acceleration/acceleration-stats/acceleration-stats.component'; +import { StateService } from '@app/services/state.service'; +import { StorageService } from '@app/services/storage.service'; +import { MenuGroup } from '@interfaces/services.interface'; +import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMap, map } from 'rxjs'; +import { IBackendInfo } from '@interfaces/websocket.interface'; +import { Acceleration, AccelerationHistoryParams } from '@interfaces/node-api.interface'; +import { AccelerationStats } from '@components/acceleration/acceleration-stats/acceleration-stats.component'; -export type ProductType = 'enterprise' | 'community' | 'mining_pool' | 'custom'; export interface IUser { username: string; email: string | null; passwordIsSet: boolean; snsId: string; - type: ProductType; + type: 'enterprise' | 'community' | 'mining_pool'; subscription_tag: string; status: 'pending' | 'verified' | 'disabled'; features: string | null; @@ -136,16 +135,16 @@ export class ServicesApiServices { return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate`, { txInput: txInput, userBid: userBid, accelerationUUID: accelerationUUID }); } - accelerateWithCashApp$(txInput: string, token: string, cashtag: string, referenceId: string, accelerationUUID: string) { - return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, accelerationUUID: accelerationUUID }); + accelerateWithCashApp$(txInput: string, token: string, cashtag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) { + return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD }); } - accelerateWithApplePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string) { - return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID }); + accelerateWithApplePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) { + return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD }); } - accelerateWithGooglePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string) { - return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID }); + accelerateWithGooglePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) { + return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD }); } getAccelerations$(): Observable { @@ -160,6 +159,29 @@ export class ServicesApiServices { return this.httpClient.get(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history`, { params: { ...params } }); } + getAllAccelerationHistory$(params: AccelerationHistoryParams, limit?: number, findTxid?: string): Observable { + const getPage$ = (page: number, accelerations: Acceleration[] = []): Observable<{ page: number, total: number, accelerations: Acceleration[] }> => { + return this.getAccelerationHistoryObserveResponse$({...params, page}).pipe( + map((response) => ({ + page, + total: parseInt(response.headers.get('X-Total-Count'), 10) || 0, + accelerations: accelerations.concat(response.body || []), + })), + switchMap(({page, total, accelerations}) => { + if (accelerations.length >= Math.min(total, limit ?? Infinity) || (findTxid && accelerations.find((acc) => acc.txid === findTxid))) { + return of({ page, total, accelerations }); + } else { + return getPage$(page + 1, accelerations); + } + }), + ); + }; + + return getPage$(1).pipe( + map(({ accelerations }) => accelerations), + ); + } + getAccelerationHistoryObserveResponse$(params: AccelerationHistoryParams): Observable { return this.httpClient.get(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history`, { params: { ...params }, observe: 'response'}); } diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 13ffc7fc5..8dd17cf75 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -1,14 +1,14 @@ import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core'; import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs'; -import { Transaction } from '../interfaces/electrs.interface'; -import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '../interfaces/websocket.interface'; -import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface'; +import { AddressTxSummary, Transaction } from '@interfaces/electrs.interface'; +import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '@interfaces/websocket.interface'; +import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '@interfaces/node-api.interface'; import { Router, NavigationStart } from '@angular/router'; import { isPlatformBrowser } from '@angular/common'; import { filter, map, scan, share, shareReplay } from 'rxjs/operators'; -import { StorageService } from './storage.service'; -import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils'; -import { ActiveFilter } from '../shared/filters.utils'; +import { StorageService } from '@app/services/storage.service'; +import { hasTouchScreen } from '@app/shared/pipes/bytes-pipe/utils'; +import { ActiveFilter } from '@app/shared/filters.utils'; export interface MarkBlockState { blockHeight?: number; @@ -78,6 +78,7 @@ export interface Env { PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string; SERVICES_API?: string; customize?: Customization; + PROD_DOMAINS: string[]; } const defaultEnv: Env = { @@ -113,6 +114,7 @@ const defaultEnv: Env = { 'PUBLIC_ACCELERATIONS': false, 'ADDITIONAL_CURRENCIES': false, 'SERVICES_API': 'https://mempool.space/api/v1/services', + 'PROD_DOMAINS': [], }; @Injectable({ @@ -159,6 +161,7 @@ export class StateService { mempoolRemovedTransactions$ = new Subject(); multiAddressTransactions$ = new Subject<{ [address: string]: { mempool: Transaction[], confirmed: Transaction[], removed: Transaction[] }}>(); blockTransactions$ = new Subject(); + walletTransactions$ = new Subject(); isLoadingWebSocket$ = new ReplaySubject(1); isLoadingMempool$ = new BehaviorSubject(true); vbytesPerSecond$ = new ReplaySubject(1); @@ -205,6 +208,10 @@ export class StateService { const browserWindow = window || {}; // @ts-ignore const browserWindowEnv = browserWindow.__env || {}; + if (browserWindowEnv.PROD_DOMAINS && typeof(browserWindowEnv.PROD_DOMAINS) === 'string') { + browserWindowEnv.PROD_DOMAINS = browserWindowEnv.PROD_DOMAINS.split(','); + } + this.env = Object.assign(defaultEnv, browserWindowEnv); if (defaultEnv.BASE_MODULE !== 'mempool') { diff --git a/frontend/src/app/services/theme.service.ts b/frontend/src/app/services/theme.service.ts index 0a0fdc505..6ed9c1027 100644 --- a/frontend/src/app/services/theme.service.ts +++ b/frontend/src/app/services/theme.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; import { Subject } from 'rxjs'; -import { defaultMempoolFeeColors, contrastMempoolFeeColors } from '../app.constants'; -import { StorageService } from './storage.service'; -import { StateService } from './state.service'; +import { defaultMempoolFeeColors, contrastMempoolFeeColors } from '@app/app.constants'; +import { StorageService } from '@app/services/storage.service'; +import { StateService } from '@app/services/state.service'; @Injectable({ providedIn: 'root' diff --git a/frontend/src/app/services/time.service.ts b/frontend/src/app/services/time.service.ts new file mode 100644 index 000000000..e4804deab --- /dev/null +++ b/frontend/src/app/services/time.service.ts @@ -0,0 +1,266 @@ +import { Injectable } from '@angular/core'; +import { DatePipe } from '@angular/common'; +import { dates } from '@app/shared/i18n/dates'; + +const intervals = { + year: 31536000, + month: 2592000, + week: 604800, + day: 86400, + hour: 3600, + minute: 60, + second: 1 +}; + +const precisionThresholds = { + year: 100, + month: 18, + week: 12, + day: 31, + hour: 48, + minute: 90, + second: 90 +}; + +@Injectable({ + providedIn: 'root' +}) +export class TimeService { + + constructor(private datePipe: DatePipe) {} + + calculate( + time: number, + kind: 'plain' | 'since' | 'until' | 'span' | 'before' | 'within', + relative: boolean = false, + precision: number = 0, + minUnit: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' = 'second', + showTooltip: boolean = false, + units: string[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'], + dateString?: string, + lowercaseStart: boolean = false, + numUnits: number = 1, + fractionDigits: number = 0, + ): { text: string, tooltip: string } { + if (time == null) { + return { text: '', tooltip: '' }; + } + + let seconds: number; + let tooltip: string = ''; + switch (kind) { + case 'since': + seconds = Math.floor((+new Date() - +new Date(dateString || time * 1000)) / 1000); + tooltip = this.datePipe.transform(new Date(dateString || time * 1000), 'yyyy-MM-dd HH:mm') || ''; + break; + case 'until': + case 'within': + seconds = (+new Date(time) - +new Date()) / 1000; + tooltip = this.datePipe.transform(new Date(time), 'yyyy-MM-dd HH:mm') || ''; + break; + default: + seconds = Math.floor(time); + tooltip = ''; + } + + if (!showTooltip || relative) { + tooltip = ''; + } + + if (seconds < 1 && kind === 'span') { + return { tooltip, text: $localize`:@@date-base.immediately:Immediately` }; + } else if (seconds < 60) { + if (relative || kind === 'since') { + if (lowercaseStart) { + return { tooltip, text: $localize`:@@date-base.just-now:Just now`.charAt(0).toLowerCase() + $localize`:@@date-base.just-now:Just now`.slice(1) }; + } + return { tooltip, text: $localize`:@@date-base.just-now:Just now` }; + } else if (kind === 'until' || kind === 'within') { + seconds = 60; + } + } + + let counter: number; + const result: string[] = []; + let usedUnits = 0; + for (const [index, unit] of units.entries()) { + let precisionUnit = units[Math.min(units.length - 1, index + precision)]; + counter = Math.floor(seconds / intervals[unit]); + const precisionCounter = Math.round(seconds / intervals[precisionUnit]); + if (precisionCounter > precisionThresholds[precisionUnit]) { + precisionUnit = unit; + } + if (units.indexOf(precisionUnit) === units.indexOf(minUnit)) { + counter = Math.max(1, counter); + } + if (counter > 0) { + let rounded; + const roundFactor = Math.pow(10,fractionDigits || 0); + if ((kind === 'until' || kind === 'within') && usedUnits < numUnits) { + rounded = Math.floor((seconds / intervals[precisionUnit]) * roundFactor) / roundFactor; + } else { + rounded = Math.round((seconds / intervals[precisionUnit]) * roundFactor) / roundFactor; + } + if ((kind !== 'until' && kind !== 'within')|| numUnits === 1) { + return { tooltip, text: this.formatTime(kind, precisionUnit, rounded) }; + } else { + if (!usedUnits) { + result.push(this.formatTime(kind, precisionUnit, rounded)); + } else { + result.push(this.formatTime('', precisionUnit, rounded)); + } + seconds -= (rounded * intervals[precisionUnit]); + usedUnits++; + if (usedUnits >= numUnits) { + return { tooltip, text: result.join(', ') }; + } + } + } + } + return { tooltip, text: 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; + } + } + } + return ''; + } +} diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index 39e9d1af3..5ec13c03f 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -1,14 +1,14 @@ import { Injectable } from '@angular/core'; import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; -import { WebsocketResponse } from '../interfaces/websocket.interface'; -import { StateService } from './state.service'; -import { Transaction } from '../interfaces/electrs.interface'; +import { WebsocketResponse } from '@interfaces/websocket.interface'; +import { StateService } from '@app/services/state.service'; +import { Transaction } from '@interfaces/electrs.interface'; import { firstValueFrom, Subscription } from 'rxjs'; -import { ApiService } from './api.service'; +import { ApiService } from '@app/services/api.service'; import { take } from 'rxjs/operators'; import { TransferState, makeStateKey } from '@angular/core'; -import { CacheService } from './cache.service'; -import { uncompressDeltaChange, uncompressTx } from '../shared/common.utils'; +import { CacheService } from '@app/services/cache.service'; +import { uncompressDeltaChange, uncompressTx } from '@app/shared/common.utils'; const OFFLINE_RETRY_AFTER_MS = 2000; const OFFLINE_PING_CHECK_AFTER_MS = 30000; @@ -34,6 +34,8 @@ export class WebsocketService { private isTrackingAddress: string | false = false; private isTrackingAddresses: string[] | false = false; private isTrackingAccelerations: boolean = false; + private isTrackingWallet: boolean = false; + private trackingWalletName: string; private trackingMempoolBlock: number; private stoppingTrackMempoolBlock: any | null = null; private latestGitCommit = ''; @@ -137,6 +139,9 @@ export class WebsocketService { if (this.isTrackingAccelerations) { this.startTrackAccelerations(); } + if (this.isTrackingWallet) { + this.startTrackingWallet(this.trackingWalletName); + } this.stateService.connectionState$.next(2); } @@ -196,6 +201,18 @@ export class WebsocketService { this.isTrackingAddresses = false; } + startTrackingWallet(walletName: string) { + this.websocketSubject.next({ 'track-wallet': walletName }); + this.isTrackingWallet = true; + this.trackingWalletName = walletName; + } + + stopTrackingWallet() { + this.websocketSubject.next({ 'track-wallet': 'stop' }); + this.isTrackingWallet = false; + this.trackingWalletName = ''; + } + startTrackAsset(asset: string) { this.websocketSubject.next({ 'track-asset': asset }); } @@ -452,6 +469,10 @@ export class WebsocketService { } } + if (response['wallet-transactions']) { + this.stateService.walletTransactions$.next(response['wallet-transactions']); + } + if (response['accelerations']) { if (response['accelerations'].accelerations) { this.stateService.accelerations$.next({ diff --git a/frontend/src/app/shared/address-utils.ts b/frontend/src/app/shared/address-utils.ts index 59c85014b..0a7f2df02 100644 --- a/frontend/src/app/shared/address-utils.ts +++ b/frontend/src/app/shared/address-utils.ts @@ -1,7 +1,7 @@ import '@angular/localize/init'; -import { ScriptInfo } from './script.utils'; -import { Vin, Vout } from '../interfaces/electrs.interface'; -import { BECH32_CHARS_LW, BASE58_CHARS, HEX_CHARS } from './regex.utils'; +import { ScriptInfo } from '@app/shared/script.utils'; +import { Vin, Vout } from '@interfaces/electrs.interface'; +import { BECH32_CHARS_LW, BASE58_CHARS, HEX_CHARS } from '@app/shared/regex.utils'; export type AddressType = 'fee' | 'empty' @@ -217,4 +217,4 @@ export class AddressTypeInfo { this.isMultisig = { m: script.template['m'], n: script.template['n'] }; } } -} \ No newline at end of file +} diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts index 8c69c2319..f329b55e4 100644 --- a/frontend/src/app/shared/common.utils.ts +++ b/frontend/src/app/shared/common.utils.ts @@ -1,5 +1,8 @@ import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed } from "../interfaces/websocket.interface"; -import { TransactionStripped } from "../interfaces/node-api.interface"; +import { TransactionStripped } from "@interfaces/node-api.interface"; +import { AmountShortenerPipe } from "@app/shared/pipes/amount-shortener.pipe"; +import { Router, ActivatedRoute } from '@angular/router'; +const amountShortenerPipe = new AmountShortenerPipe(); export function isMobile(): boolean { return (window.innerWidth <= 767.98); @@ -184,6 +187,33 @@ export function uncompressDeltaChange(block: number, delta: MempoolBlockDeltaCom }; } +export function renderSats(value: number, network: string, mode: 'sats' | 'btc' | 'auto' = 'auto'): string { + let prefix = ''; + switch (network) { + case 'liquid': + prefix = 'L'; + break; + case 'liquidtestnet': + prefix = 'tL'; + break; + case 'testnet': + case 'testnet4': + prefix = 't'; + break; + case 'signet': + prefix = 's'; + break; + } + if (mode === 'btc' || (mode === 'auto' && value >= 1000000)) { + return `${amountShortenerPipe.transform(value / 100000000, 2)} ${prefix}BTC`; + } else { + if (prefix.length) { + prefix += '-'; + } + return `${amountShortenerPipe.transform(value, 2)} ${prefix}sats`; + } +} + export function insecureRandomUUID(): string { const hexDigits = '0123456789abcdef'; const uuidLengths = [8, 4, 4, 4, 12]; @@ -197,6 +227,29 @@ export function insecureRandomUUID(): string { return uuid.slice(0, -1); } +export function sleep$(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, ms); + }); +} + +export function handleDemoRedirect(route: ActivatedRoute, router: Router) { + route.queryParams + .subscribe(params => { + if (params.next) { + const path = ['/', '/acceleration', '/mining', '/lightning']; + const index = path.indexOf(params.next); + if (index >= 0) { + const nextPath = path[(index + 1) % path.length]; + setTimeout(() => { window.location.replace(`${params.next}?next=${nextPath}`) }, 15000); + } + } + } + ); +} + // https://stackoverflow.com/a/60467595 export function md5(inputString): string { var hc="0123456789abcdef"; diff --git a/frontend/src/app/shared/components/address-type/address-type.component.ts b/frontend/src/app/shared/components/address-type/address-type.component.ts index 1a2456c07..d0ae7a8f1 100644 --- a/frontend/src/app/shared/components/address-type/address-type.component.ts +++ b/frontend/src/app/shared/components/address-type/address-type.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -import { AddressTypeInfo } from '../../address-utils'; +import { AddressTypeInfo } from '@app/shared/address-utils'; @Component({ selector: 'app-address-type', diff --git a/frontend/src/app/shared/components/btc/btc.component.ts b/frontend/src/app/shared/components/btc/btc.component.ts index 4e62b07ff..a87be7a4f 100644 --- a/frontend/src/app/shared/components/btc/btc.component.ts +++ b/frontend/src/app/shared/components/btc/btc.component.ts @@ -1,6 +1,6 @@ import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { Subscription } from 'rxjs'; -import { StateService } from '../../../services/state.service'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-btc', diff --git a/frontend/src/app/shared/components/fee-rate/fee-rate.component.ts b/frontend/src/app/shared/components/fee-rate/fee-rate.component.ts index b1d143e7f..9a2565cec 100644 --- a/frontend/src/app/shared/components/fee-rate/fee-rate.component.ts +++ b/frontend/src/app/shared/components/fee-rate/fee-rate.component.ts @@ -1,6 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; -import { StateService } from '../../../services/state.service'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-fee-rate', diff --git a/frontend/src/app/shared/components/geolocation/geolocation.component.ts b/frontend/src/app/shared/components/geolocation/geolocation.component.ts index 1a498a1b2..2d9aa684b 100644 --- a/frontend/src/app/shared/components/geolocation/geolocation.component.ts +++ b/frontend/src/app/shared/components/geolocation/geolocation.component.ts @@ -1,5 +1,5 @@ import { Component, Input, OnChanges } from '@angular/core'; -import { convertRegion, getFlagEmoji } from '../../common.utils'; +import { convertRegion, getFlagEmoji } from '@app/shared/common.utils'; export interface GeolocationData { country: string; @@ -70,6 +70,12 @@ export class GeolocationComponent implements OnChanges { if (this.type === 'node') { const city = this.data.city ? this.data.city : ''; + // Handle city-states like Singapore or Hong Kong + if (city && city === this.data?.country) { + this.formattedLocation = `${this.data.country} ${getFlagEmoji(this.data.iso)}`; + return; + } + // City this.formattedLocation = `${city}`; diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.html b/frontend/src/app/shared/components/global-footer/global-footer.component.html index a2e7286e0..d82bb8062 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.html +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.html @@ -5,7 +5,7 @@
@if (!enterpriseInfo?.footer_img) {

- Explore the full Bitcoin ecosystem - ® + @if (officialMempoolSpace) { + Explore the full Bitcoin ecosystem + ® + } @else { + Be your own explorer + + }

}
@@ -27,29 +32,38 @@
+
+ +
@if (!env.customize?.theme) { - @if (!env.customize?.theme) { -
- +
+ +
} @if (!enterpriseInfo?.footer_img) { -

- Explore the full Bitcoin ecosystem - ® + @if (officialMempoolSpace) { + Explore the full Bitcoin ecosystem + ® + } @else { + Be your own explorer + + }

}
diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.scss b/frontend/src/app/shared/components/global-footer/global-footer.component.scss index e0daf4f4c..bf47d5489 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.scss +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.scss @@ -76,6 +76,11 @@ footer .selector { display: inline-block; } +footer .add-margin { + margin-left: 5px; + margin-right: 5px; +} + footer .row.link-tree { max-width: 1140px; margin: 0 auto; @@ -154,7 +159,7 @@ footer .nowrap { display: block; } -@media (min-width: 951px) { +@media (min-width: 1020px) { :host-context(.ltr-layout) .language-selector { float: right !important; } @@ -172,7 +177,24 @@ footer .nowrap { } .services { - @media (min-width: 951px) and (max-width: 1147px) { + @media (min-width: 1300px) { + :host-context(.ltr-layout) .language-selector { + float: right !important; + } + :host-context(.rtl-layout) .language-selector { + float: left !important; + } + + .explore-tagline-desktop { + display: block; + } + + .explore-tagline-mobile { + display: none; + } + } + + @media (max-width: 1300px) { :host-context(.ltr-layout) .services .language-selector { float: none !important; } @@ -248,7 +270,7 @@ footer .nowrap { } -@media (max-width: 950px) { +@media (max-width: 1019px) { .main-logo { width: 220px; @@ -287,7 +309,7 @@ footer .nowrap { } } -@media (max-width: 1147px) { +@media (max-width: 1300px) { .services.main-logo { width: 220px; diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.ts b/frontend/src/app/shared/components/global-footer/global-footer.component.ts index 50f9a53d7..2fad96d24 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.ts +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.ts @@ -2,13 +2,13 @@ import { Input, ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, O import { Router, ActivatedRoute } from '@angular/router'; import { Observable, merge, of, Subject, Subscription } from 'rxjs'; import { tap, takeUntil } from 'rxjs/operators'; -import { Env, StateService } from '../../../services/state.service'; -import { IBackendInfo } from '../../../interfaces/websocket.interface'; -import { LanguageService } from '../../../services/language.service'; -import { NavigationService } from '../../../services/navigation.service'; -import { StorageService } from '../../../services/storage.service'; -import { WebsocketService } from '../../../services/websocket.service'; -import { EnterpriseService } from '../../../services/enterprise.service'; +import { Env, StateService } from '@app/services/state.service'; +import { IBackendInfo } from '@interfaces/websocket.interface'; +import { LanguageService } from '@app/services/language.service'; +import { NavigationService } from '@app/services/navigation.service'; +import { StorageService } from '@app/services/storage.service'; +import { WebsocketService } from '@app/services/websocket.service'; +import { EnterpriseService } from '@app/services/enterprise.service'; @Component({ selector: 'app-global-footer', diff --git a/frontend/src/app/shared/components/sats/sats.component.ts b/frontend/src/app/shared/components/sats/sats.component.ts index 39be66ecd..c9fbc741f 100644 --- a/frontend/src/app/shared/components/sats/sats.component.ts +++ b/frontend/src/app/shared/components/sats/sats.component.ts @@ -1,6 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; import { Subscription } from 'rxjs'; -import { StateService } from '../../../services/state.service'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-sats', diff --git a/frontend/src/app/shared/components/testnet-alert/testnet-alert.component.ts b/frontend/src/app/shared/components/testnet-alert/testnet-alert.component.ts index 0672fd04b..47d6bbe34 100644 --- a/frontend/src/app/shared/components/testnet-alert/testnet-alert.component.ts +++ b/frontend/src/app/shared/components/testnet-alert/testnet-alert.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { StorageService } from '../../../services/storage.service'; -import { StateService } from '../../../services/state.service'; +import { StorageService } from '@app/services/storage.service'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-testnet-alert', diff --git a/frontend/src/app/shared/components/weight-directives/weight-directives.ts b/frontend/src/app/shared/components/weight-directives/weight-directives.ts index 879b6bee6..7f0d65ff4 100644 --- a/frontend/src/app/shared/components/weight-directives/weight-directives.ts +++ b/frontend/src/app/shared/components/weight-directives/weight-directives.ts @@ -1,6 +1,6 @@ import { Directive, OnDestroy, TemplateRef, ViewContainerRef } from '@angular/core'; import { Subscription } from 'rxjs'; -import { StateService } from '../../../services/state.service'; +import { StateService } from '@app/services/state.service'; function createRateUnitDirective(checkFn: (rateUnit: string) => boolean): any { @Directive() @@ -42,4 +42,4 @@ function createRateUnitDirective(checkFn: (rateUnit: string) => boolean): any { export class OnlyVsizeDirective extends createRateUnitDirective(rateUnit => rateUnit !== 'wu') {} @Directive({ selector: '[only-weight]' }) -export class OnlyWeightDirective extends createRateUnitDirective(rateUnit => rateUnit === 'wu') {} \ No newline at end of file +export class OnlyWeightDirective extends createRateUnitDirective(rateUnit => rateUnit === 'wu') {} diff --git a/frontend/src/app/shared/ord/inscription.utils.ts b/frontend/src/app/shared/ord/inscription.utils.ts new file mode 100644 index 000000000..08ecc316a --- /dev/null +++ b/frontend/src/app/shared/ord/inscription.utils.ts @@ -0,0 +1,425 @@ +/* +MIT License + +Copyright (c) 2024 HAUS HOPPE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +*/ + +// Adapted from https://github.com/ordpool-space/ordpool-parser/tree/ce04d7a5b6bb1cf37b9fdadd77ba430f5bd6e7d6/src +// Utils functions to decode ord inscriptions + +export const OP_FALSE = 0x00; +export const OP_IF = 0x63; +export const OP_0 = 0x00; + +export const OP_PUSHBYTES_3 = 0x03; // 3 -- not an actual opcode, but used in documentation --> pushes the next 3 bytes onto the stack. +export const OP_PUSHDATA1 = 0x4c; // 76 -- The next byte contains the number of bytes to be pushed onto the stack. +export const OP_PUSHDATA2 = 0x4d; // 77 -- The next two bytes contain the number of bytes to be pushed onto the stack in little endian order. +export const OP_PUSHDATA4 = 0x4e; // 78 -- The next four bytes contain the number of bytes to be pushed onto the stack in little endian order. +export const OP_ENDIF = 0x68; // 104 -- Ends an if/else block. + +export const OP_1NEGATE = 0x4f; // 79 -- The number -1 is pushed onto the stack. +export const OP_RESERVED = 0x50; // 80 -- Transaction is invalid unless occuring in an unexecuted OP_IF branch +export const OP_PUSHNUM_1 = 0x51; // 81 -- also known as OP_1 +export const OP_PUSHNUM_2 = 0x52; // 82 -- also known as OP_2 +export const OP_PUSHNUM_3 = 0x53; // 83 -- also known as OP_3 +export const OP_PUSHNUM_4 = 0x54; // 84 -- also known as OP_4 +export const OP_PUSHNUM_5 = 0x55; // 85 -- also known as OP_5 +export const OP_PUSHNUM_6 = 0x56; // 86 -- also known as OP_6 +export const OP_PUSHNUM_7 = 0x57; // 87 -- also known as OP_7 +export const OP_PUSHNUM_8 = 0x58; // 88 -- also known as OP_8 +export const OP_PUSHNUM_9 = 0x59; // 89 -- also known as OP_9 +export const OP_PUSHNUM_10 = 0x5a; // 90 -- also known as OP_10 +export const OP_PUSHNUM_11 = 0x5b; // 91 -- also known as OP_11 +export const OP_PUSHNUM_12 = 0x5c; // 92 -- also known as OP_12 +export const OP_PUSHNUM_13 = 0x5d; // 93 -- also known as OP_13 +export const OP_PUSHNUM_14 = 0x5e; // 94 -- also known as OP_14 +export const OP_PUSHNUM_15 = 0x5f; // 95 -- also known as OP_15 +export const OP_PUSHNUM_16 = 0x60; // 96 -- also known as OP_16 + +export const OP_RETURN = 0x6a; // 106 -- a standard way of attaching extra data to transactions is to add a zero-value output with a scriptPubKey consisting of OP_RETURN followed by data + +//////////////////////////// Helper /////////////////////////////// + +/** + * Inscriptions may include fields before an optional body. Each field consists of two data pushes, a tag and a value. + * Currently, there are six defined fields: + */ +export const knownFields = { + // content_type, with a tag of 1, whose value is the MIME type of the body. + content_type: 0x01, + + // pointer, with a tag of 2, see pointer docs: https://docs.ordinals.com/inscriptions/pointer.html + pointer: 0x02, + + // parent, with a tag of 3, see provenance docs: https://docs.ordinals.com/inscriptions/provenance.html + parent: 0x03, + + // metadata, with a tag of 5, see metadata docs: https://docs.ordinals.com/inscriptions/metadata.html + metadata: 0x05, + + // metaprotocol, with a tag of 7, whose value is the metaprotocol identifier. + metaprotocol: 0x07, + + // content_encoding, with a tag of 9, whose value is the encoding of the body. + content_encoding: 0x09, + + // delegate, with a tag of 11, see delegate docs: https://docs.ordinals.com/inscriptions/delegate.html + delegate: 0xb +} + +/** + * Retrieves the value for a given field from an array of field objects. + * It returns the value of the first object where the tag matches the specified field. + * + * @param fields - An array of objects containing tag and value properties. + * @param field - The field number to search for. + * @returns The value associated with the first matching field, or undefined if no match is found. + */ +export function getKnownFieldValue(fields: { tag: number; value: Uint8Array }[], field: number): Uint8Array | undefined { + const knownField = fields.find(x => + x.tag === field); + + if (knownField === undefined) { + return undefined; + } + + return knownField.value; +} + +/** + * Retrieves the values for a given field from an array of field objects. + * It returns the values of all objects where the tag matches the specified field. + * + * @param fields - An array of objects containing tag and value properties. + * @param field - The field number to search for. + * @returns An array of Uint8Array values associated with the matching fields. If no matches are found, an empty array is returned. + */ +export function getKnownFieldValues(fields: { tag: number; value: Uint8Array }[], field: number): Uint8Array[] { + const knownFields = fields.filter(x => + x.tag === field + ); + + return knownFields.map(field => field.value); +} + +/** + * Searches for the next position of the ordinal inscription mark (0063036f7264) + * within the raw transaction data, starting from a given position. + * + * This function looks for a specific sequence of 6 bytes that represents the start of an ordinal inscription. + * If the sequence is found, the function returns the index immediately following the inscription mark. + * If the sequence is not found, the function returns -1, indicating no inscription mark was found. + * + * Note: This function uses a simple hardcoded approach based on the fixed length of the inscription mark. + * + * @returns The position immediately after the inscription mark, or -1 if not found. + */ +export function getNextInscriptionMark(raw: Uint8Array, startPosition: number): number { + + // OP_FALSE + // OP_IF + // OP_PUSHBYTES_3: This pushes the next 3 bytes onto the stack. + // 0x6f, 0x72, 0x64: These bytes translate to the ASCII string "ord" + const inscriptionMark = new Uint8Array([OP_FALSE, OP_IF, OP_PUSHBYTES_3, 0x6f, 0x72, 0x64]); + + for (let index = startPosition; index <= raw.length - 6; index++) { + if (raw[index] === inscriptionMark[0] && + raw[index + 1] === inscriptionMark[1] && + raw[index + 2] === inscriptionMark[2] && + raw[index + 3] === inscriptionMark[3] && + raw[index + 4] === inscriptionMark[4] && + raw[index + 5] === inscriptionMark[5]) { + return index + 6; + } + } + + return -1; +} + +/////////////////////////////// Reader /////////////////////////////// + +/** + * Reads a specified number of bytes from a Uint8Array starting from a given pointer. + * + * @param raw - The Uint8Array from which bytes are to be read. + * @param pointer - The position in the array from where to start reading. + * @param n - The number of bytes to read. + * @returns A tuple containing the read bytes as Uint8Array and the updated pointer position. + */ +export function readBytes(raw: Uint8Array, pointer: number, n: number): [Uint8Array, number] { + const slice = raw.slice(pointer, pointer + n); + return [slice, pointer + n]; +} + +/** + * Reads data based on the Bitcoin script push opcode starting from a specified pointer in the raw data. + * Handles different opcodes and direct push (where the opcode itself signifies the number of bytes to push). + * + * @param raw - The raw transaction data as a Uint8Array. + * @param pointer - The current position in the raw data array. + * @returns A tuple containing the read data as Uint8Array and the updated pointer position. + */ +export function readPushdata(raw: Uint8Array, pointer: number): [Uint8Array, number] { + + let [opcodeSlice, newPointer] = readBytes(raw, pointer, 1); + const opcode = opcodeSlice[0]; + + // Handle the special case of OP_0 (0x00) which pushes an empty array (interpreted as zero) + // fixes #18 + if (opcode === OP_0) { + return [new Uint8Array(), newPointer]; + } + + // Handle the special case of OP_1NEGATE (-1) + if (opcode === OP_1NEGATE) { + // OP_1NEGATE pushes the value -1 onto the stack, represented as 0x81 in Bitcoin Script + return [new Uint8Array([0x81]), newPointer]; + } + + // Handle minimal push numbers OP_PUSHNUM_1 (0x51) to OP_PUSHNUM_16 (0x60) + // which are used to push the values 0x01 (decimal 1) through 0x10 (decimal 16) onto the stack. + // To get the value, we can subtract OP_RESERVED (0x50) from the opcode to get the value to be pushed. + if (opcode >= OP_PUSHNUM_1 && opcode <= OP_PUSHNUM_16) { + // Convert opcode to corresponding byte value + const byteValue = opcode - OP_RESERVED; + return [Uint8Array.from([byteValue]), newPointer]; + } + + // Handle direct push of 1 to 75 bytes (OP_PUSHBYTES_1 to OP_PUSHBYTES_75) + if (1 <= opcode && opcode <= 75) { + return readBytes(raw, newPointer, opcode); + } + + let numBytes: number; + switch (opcode) { + case OP_PUSHDATA1: numBytes = 1; break; + case OP_PUSHDATA2: numBytes = 2; break; + case OP_PUSHDATA4: numBytes = 4; break; + default: + throw new Error(`Invalid push opcode ${opcode} at position ${pointer}`); + } + + let [dataSizeArray, nextPointer] = readBytes(raw, newPointer, numBytes); + let dataSize = littleEndianBytesToNumber(dataSizeArray); + return readBytes(raw, nextPointer, dataSize); +} + +//////////////////////////// Conversion //////////////////////////// + +/** + * Converts a Uint8Array containing UTF-8 encoded data to a normal a UTF-16 encoded string. + * + * @param bytes - The Uint8Array containing UTF-8 encoded data. + * @returns The corresponding UTF-16 encoded JavaScript string. + */ +export function bytesToUnicodeString(bytes: Uint8Array): string { + const decoder = new TextDecoder('utf-8'); + return decoder.decode(bytes); +} + +/** + * Convert a Uint8Array to a string by treating each byte as a character code. + * It avoids interpreting bytes as UTF-8 encoded sequences. + * --> Again: it ignores UTF-8 encoding, which is necessary for binary content! + * + * Note: This method is different from just using `String.fromCharCode(...combinedData)` which can + * cause a "Maximum call stack size exceeded" error for large arrays due to the limitation of + * the spread operator in JavaScript. (previously the parser broke here, because of large content) + * + * @param bytes - The byte array to convert. + * @returns The resulting string where each byte value is treated as a direct character code. + */ +export function bytesToBinaryString(bytes: Uint8Array): string { + let resultStr = ''; + for (let i = 0; i < bytes.length; i++) { + resultStr += String.fromCharCode(bytes[i]); + } + return resultStr; +} + +/** + * Converts a hexadecimal string to a Uint8Array. + * + * @param hex - A string of hexadecimal characters. + * @returns A Uint8Array representing the hex string. + */ +export function hexToBytes(hex: string): Uint8Array { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0, j = 0; i < hex.length; i += 2, j++) { + bytes[j] = parseInt(hex.slice(i, i + 2), 16); + } + return bytes; +} + +/** + * Converts a Uint8Array to a hexadecimal string. + * + * @param bytes - A Uint8Array to convert. + * @returns A string of hexadecimal characters representing the byte array. + */ +export function bytesToHex(bytes: Uint8Array): string { + if (!bytes) { + return null; + } + return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join(''); +} + +/** + * Converts a little-endian byte array to a JavaScript number. + * + * This function interprets the provided bytes in little-endian format, where the least significant byte comes first. + * It constructs an integer value representing the number encoded by the bytes. + * + * @param byteArray - An array containing the bytes in little-endian format. + * @returns The number represented by the byte array. + */ +export function littleEndianBytesToNumber(byteArray: Uint8Array): number { + let number = 0; + for (let i = 0; i < byteArray.length; i++) { + // Extract each byte from byteArray, shift it to the left by 8 * i bits, and combine it with number. + // The shifting accounts for the little-endian format where the least significant byte comes first. + number |= byteArray[i] << (8 * i); + } + return number; +} + +/** + * Concatenates multiple Uint8Array objects into a single Uint8Array. + * + * @param arrays - An array of Uint8Array objects to concatenate. + * @returns A new Uint8Array containing the concatenated results of the input arrays. + */ +export function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array { + if (arrays.length === 0) { + return new Uint8Array(); + } + + const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0); + const result = new Uint8Array(totalLength); + let offset = 0; + + for (const array of arrays) { + result.set(array, offset); + offset += array.length; + } + + return result; +} + +////////////////////////////// Inscription /////////////////////////// + +export interface Inscription { + body?: Uint8Array; + is_cropped?: boolean; + body_length?: number; + content_type?: Uint8Array; + content_type_str?: string; + delegate_txid?: string; +} + +/** + * Extracts fields from the raw data until OP_0 is encountered. + * + * @param raw - The raw data to read. + * @param pointer - The current pointer where the reading starts. + * @returns An array of fields and the updated pointer position. + */ +export function extractFields(raw: Uint8Array, pointer: number): [{ tag: number; value: Uint8Array }[], number] { + + const fields: { tag: number; value: Uint8Array }[] = []; + let newPointer = pointer; + let slice: Uint8Array; + + while (newPointer < raw.length && + // normal inscription - content follows now + (raw[newPointer] !== OP_0) && + // delegate - inscription has no further content and ends directly here + (raw[newPointer] !== OP_ENDIF) + ) { + + // tags are encoded by ord as single-byte data pushes, but are accepted by ord as either single-byte pushes, or as OP_NUM data pushes. + // tags greater than or equal to 256 should be encoded as little endian integers with trailing zeros omitted. + // see: https://github.com/ordinals/ord/issues/2505 + [slice, newPointer] = readPushdata(raw, newPointer); + const tag = slice.length === 1 ? slice[0] : littleEndianBytesToNumber(slice); + + [slice, newPointer] = readPushdata(raw, newPointer); + const value = slice; + + fields.push({ tag, value }); + } + + return [fields, newPointer]; +} + + +/** + * Extracts inscription data starting from the current pointer. + * @param raw - The raw data to read. + * @param pointer - The current pointer where the reading starts. + * @returns The parsed inscription or nullx + */ +export function extractInscriptionData(raw: Uint8Array, pointer: number): Inscription | null { + + try { + + let fields: { tag: number; value: Uint8Array }[]; + let newPointer: number; + let slice: Uint8Array; + + [fields, newPointer] = extractFields(raw, pointer); + + // Now we are at the beginning of the body + // (or at the end of the raw data if there's no body) + if (newPointer < raw.length && raw[newPointer] === OP_0) { + newPointer++; // Skip OP_0 + } + + // Collect body data until OP_ENDIF + const data: Uint8Array[] = []; + while (newPointer < raw.length && raw[newPointer] !== OP_ENDIF) { + [slice, newPointer] = readPushdata(raw, newPointer); + data.push(slice); + } + + const combinedLengthOfAllArrays = data.reduce((acc, curr) => acc + curr.length, 0); + let combinedData = new Uint8Array(combinedLengthOfAllArrays); + + // Copy all segments from data into combinedData, forming a single contiguous Uint8Array + let idx = 0; + for (const segment of data) { + combinedData.set(segment, idx); + idx += segment.length; + } + + const contentTypeRaw = getKnownFieldValue(fields, knownFields.content_type); + let contentType: string; + + if (!contentTypeRaw) { + contentType = 'undefined'; + } else { + contentType = bytesToUnicodeString(contentTypeRaw); + } + + return { + content_type_str: contentType, + body: combinedData.slice(0, 100_000), // Limit body to 100 kB for now + is_cropped: combinedData.length > 100_000, + body_length: combinedData.length, + delegate_txid: getKnownFieldValue(fields, knownFields.delegate) ? bytesToHex(getKnownFieldValue(fields, knownFields.delegate).reverse()) : null + }; + + } catch (ex) { + return null; + } +} \ No newline at end of file diff --git a/frontend/src/app/shared/ord/rune.utils.ts b/frontend/src/app/shared/ord/rune.utils.ts new file mode 100644 index 000000000..3cd9617e4 --- /dev/null +++ b/frontend/src/app/shared/ord/rune.utils.ts @@ -0,0 +1,255 @@ +import { Transaction } from '@interfaces/electrs.interface'; + +export const U128_MAX_BIGINT = 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffffn; + +export class RuneId { + block: number; + index: number; + + constructor(block: number, index: number) { + this.block = block; + this.index = index; + } + + toString(): string { + return `${this.block}:${this.index}`; + } +} + +export type Etching = { + divisibility?: number; + premine?: bigint; + symbol?: string; + terms?: { + cap?: bigint; + amount?: bigint; + offset?: { + start?: bigint; + end?: bigint; + }; + height?: { + start?: bigint; + end?: bigint; + }; + }; + turbo?: boolean; + name?: string; + spacedName?: string; + supply?: bigint; +}; + +export type Edict = { + id: RuneId; + amount: bigint; + output: number; +}; + +export type Runestone = { + mint?: RuneId; + pointer?: number; + edicts?: Edict[]; + etching?: Etching; +}; + +type Message = { + fields: Record; + edicts: Edict[]; +} + +export const UNCOMMON_GOODS: Etching = { + divisibility: 0, + premine: 0n, + symbol: '⧉', + terms: { + cap: U128_MAX_BIGINT, + amount: 1n, + offset: { + start: 0n, + end: 0n, + }, + height: { + start: 840000n, + end: 1050000n, + }, + }, + turbo: false, + name: 'UNCOMMONGOODS', + spacedName: 'UNCOMMON•GOODS', + supply: U128_MAX_BIGINT, +}; + +enum Tag { + Body = 0, + Flags = 2, + Rune = 4, + Premine = 6, + Cap = 8, + Amount = 10, + HeightStart = 12, + HeightEnd = 14, + OffsetStart = 16, + OffsetEnd = 18, + Mint = 20, + Pointer = 22, + Cenotaph = 126, + + Divisibility = 1, + Spacers = 3, + Symbol = 5, + Nop = 127, +} + +const Flag = { + ETCHING: 1n, + TERMS: 1n << 1n, + TURBO: 1n << 2n, + CENOTAPH: 1n << 127n, +}; + +function hexToBytes(hex: string): Uint8Array { + return new Uint8Array(hex.match(/.{2}/g).map((byte) => parseInt(byte, 16))); +} + +function decodeLEB128(bytes: Uint8Array): bigint[] { + const integers: bigint[] = []; + let index = 0; + while (index < bytes.length) { + let value = BigInt(0); + let shift = 0; + let byte: number; + do { + byte = bytes[index++]; + value |= BigInt(byte & 0x7f) << BigInt(shift); + shift += 7; + } while (byte & 0x80); + integers.push(value); + } + return integers; +} + +function integersToMessage(integers: bigint[]): Message { + const message = { + fields: {}, + edicts: [], + }; + let inBody = false; + while (integers.length) { + if (!inBody) { + // The integers are interpreted as a sequence of tag/value pairs, with duplicate tags appending their value to the field value. + const tag: Tag = Number(integers.shift()); + if (tag === Tag.Body) { + inBody = true; + } else { + const value = integers.shift(); + if (message.fields[tag]) { + message.fields[tag].push(value); + } else { + message.fields[tag] = [value]; + } + } + } else { + // If a tag with value zero is encountered, all following integers are interpreted as a series of four-integer edicts, each consisting of a rune ID block height, rune ID transaction index, amount, and output. + const height = integers.shift(); + const txIndex = integers.shift(); + const amount = integers.shift(); + const output = integers.shift(); + message.edicts.push({ + id: new RuneId(Number(height), Number(txIndex)), + amount, + output, + }); + } + } + return message; +} + +function parseRuneName(rune: bigint): string { + let name = ''; + rune += 1n; + while (rune > 0n) { + name = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[Number((rune - 1n) % 26n)] + name; + rune = (rune - 1n) / 26n; + } + return name; +} + +function spaceRuneName(name: string, spacers: bigint): string { + let i = 0; + let spacedName = ''; + while (spacers > 0n || i < name.length) { + spacedName += name[i]; + if (spacers & 1n) { + spacedName += '•'; + } + if (spacers > 0n) { + spacers >>= 1n; + } + i++; + } + return spacedName; +} + +function messageToRunestone(message: Message): Runestone { + let etching: Etching | undefined; + let mint: RuneId | undefined; + let pointer: number | undefined; + + const flags = message.fields[Tag.Flags]?.[0] || 0n; + if (flags & Flag.ETCHING) { + const hasTerms = (flags & Flag.TERMS) > 0n; + const isTurbo = (flags & Flag.TURBO) > 0n; + const name = parseRuneName(message.fields[Tag.Rune]?.[0] ?? 0n); + etching = { + divisibility: Number(message.fields[Tag.Divisibility]?.[0] ?? 0n), + premine: message.fields[Tag.Premine]?.[0], + symbol: message.fields[Tag.Symbol]?.[0] ? String.fromCodePoint(Number(message.fields[Tag.Symbol][0])) : '¤', + terms: hasTerms ? { + cap: message.fields[Tag.Cap]?.[0], + amount: message.fields[Tag.Amount]?.[0], + offset: { + start: message.fields[Tag.OffsetStart]?.[0], + end: message.fields[Tag.OffsetEnd]?.[0], + }, + height: { + start: message.fields[Tag.HeightStart]?.[0], + end: message.fields[Tag.HeightEnd]?.[0], + }, + } : undefined, + turbo: isTurbo, + name, + spacedName: spaceRuneName(name, message.fields[Tag.Spacers]?.[0] ?? 0n), + }; + etching.supply = ( + (etching.terms?.cap ?? 0n) * (etching.terms?.amount ?? 0n) + ) + (etching.premine ?? 0n); + } + const mintField = message.fields[Tag.Mint]; + if (mintField) { + mint = new RuneId(Number(mintField[0]), Number(mintField[1])); + } + const pointerField = message.fields[Tag.Pointer]; + if (pointerField) { + pointer = Number(pointerField[0]); + } + return { + mint, + pointer, + edicts: message.edicts, + etching, + }; +} + +export function decipherRunestone(tx: Transaction): Runestone | void { + const payload = tx.vout.find((vout) => vout.scriptpubkey.startsWith('6a5d'))?.scriptpubkey_asm.replace(/OP_\w+|\s/g, ''); + if (!payload) { + return; + } + try { + const integers = decodeLEB128(hexToBytes(payload)); + const message = integersToMessage(integers); + return messageToRunestone(message); + } catch (error) { + console.error(error); + return; + } +} diff --git a/frontend/src/app/shared/pipes/bitcoinsatoshis.pipe.ts b/frontend/src/app/shared/pipes/bitcoinsatoshis.pipe.ts index 7065b5138..7e785e9c8 100644 --- a/frontend/src/app/shared/pipes/bitcoinsatoshis.pipe.ts +++ b/frontend/src/app/shared/pipes/bitcoinsatoshis.pipe.ts @@ -8,7 +8,7 @@ export class BitcoinsatoshisPipe implements PipeTransform { constructor(private sanitizer: DomSanitizer) { } - transform(value: string): SafeHtml { + transform(value: string, firstPartClass?: string): SafeHtml { const newValue = this.insertSpaces(parseFloat(value || '0').toFixed(8)); const position = (newValue || '0').search(/[1-9]/); @@ -16,7 +16,7 @@ export class BitcoinsatoshisPipe implements PipeTransform { const secondPart = newValue.slice(position); return this.sanitizer.bypassSecurityTrustHtml( - `${firstPart}${secondPart}` + `${firstPart}${secondPart}` ); } diff --git a/frontend/src/app/shared/pipes/bytes-pipe/bytes.pipe.ts b/frontend/src/app/shared/pipes/bytes-pipe/bytes.pipe.ts index b2140f0dc..7870572bb 100644 --- a/frontend/src/app/shared/pipes/bytes-pipe/bytes.pipe.ts +++ b/frontend/src/app/shared/pipes/bytes-pipe/bytes.pipe.ts @@ -1,6 +1,6 @@ /* tslint:disable */ import { Pipe, PipeTransform } from '@angular/core'; -import { isNumberFinite, isPositive, isInteger, toDecimal, toSigFigs } from './utils'; +import { isNumberFinite, isPositive, isInteger, toDecimal, toSigFigs } from '@app/shared/pipes/bytes-pipe/utils'; export type ByteUnit = 'B' | 'kB' | 'MB' | 'GB' | 'TB'; diff --git a/frontend/src/app/shared/pipes/bytes-pipe/vbytes.pipe.ts b/frontend/src/app/shared/pipes/bytes-pipe/vbytes.pipe.ts index b88791851..4af011201 100644 --- a/frontend/src/app/shared/pipes/bytes-pipe/vbytes.pipe.ts +++ b/frontend/src/app/shared/pipes/bytes-pipe/vbytes.pipe.ts @@ -1,6 +1,6 @@ /* tslint:disable */ import { Pipe, PipeTransform } from '@angular/core'; -import { isNumberFinite, isPositive, isInteger, toDecimal } from './utils'; +import { isNumberFinite, isPositive, isInteger, toDecimal } from '@app/shared/pipes/bytes-pipe/utils'; export type ByteUnit = 'vB' | 'kvB' | 'MvB' | 'GvB' | 'TvB'; diff --git a/frontend/src/app/shared/pipes/bytes-pipe/wubytes.pipe.ts b/frontend/src/app/shared/pipes/bytes-pipe/wubytes.pipe.ts index b6566ac0a..ab82dea01 100644 --- a/frontend/src/app/shared/pipes/bytes-pipe/wubytes.pipe.ts +++ b/frontend/src/app/shared/pipes/bytes-pipe/wubytes.pipe.ts @@ -1,6 +1,6 @@ /* tslint:disable */ import { Pipe, PipeTransform } from '@angular/core'; -import { isNumberFinite, isPositive, isInteger, toDecimal } from './utils'; +import { isNumberFinite, isPositive, isInteger, toDecimal } from '@app/shared/pipes/bytes-pipe/utils'; export type ByteUnit = 'WU' | 'kWU' | 'MWU' | 'GWU' | 'TWU'; diff --git a/frontend/src/app/shared/pipes/fiat-currency.pipe.ts b/frontend/src/app/shared/pipes/fiat-currency.pipe.ts index 2ae796d2b..701426134 100644 --- a/frontend/src/app/shared/pipes/fiat-currency.pipe.ts +++ b/frontend/src/app/shared/pipes/fiat-currency.pipe.ts @@ -1,7 +1,7 @@ import { formatCurrency, getCurrencySymbol } from '@angular/common'; import { Inject, LOCALE_ID, Pipe, PipeTransform } from '@angular/core'; import { Subscription } from 'rxjs'; -import { StateService } from '../../services/state.service'; +import { StateService } from '@app/services/state.service'; @Pipe({ name: 'fiatCurrency' @@ -23,10 +23,10 @@ export class FiatCurrencyPipe implements PipeTransform { const digits = args[0] || 1; const currency = args[1] || this.currency || 'USD'; - if (num >= 1000) { + if (Math.abs(num) >= 1000) { return new Intl.NumberFormat(this.locale, { style: 'currency', currency, maximumFractionDigits: 0 }).format(num); } else { return new Intl.NumberFormat(this.locale, { style: 'currency', currency }).format(num); } } -} \ No newline at end of file +} diff --git a/frontend/src/app/shared/pipes/fiat-shortener.pipe.ts b/frontend/src/app/shared/pipes/fiat-shortener.pipe.ts index 4ce171054..7651676b4 100644 --- a/frontend/src/app/shared/pipes/fiat-shortener.pipe.ts +++ b/frontend/src/app/shared/pipes/fiat-shortener.pipe.ts @@ -1,7 +1,7 @@ import { formatCurrency, getCurrencySymbol } from '@angular/common'; import { Inject, LOCALE_ID, Pipe, PipeTransform } from '@angular/core'; import { Subscription } from 'rxjs'; -import { StateService } from '../../services/state.service'; +import { StateService } from '@app/services/state.service'; @Pipe({ name: 'fiatShortener' @@ -44,4 +44,4 @@ export class FiatShortenerPipe implements PipeTransform { return result + item.symbol; } -} \ No newline at end of file +} diff --git a/frontend/src/app/shared/pipes/relative-url/relative-url.pipe.ts b/frontend/src/app/shared/pipes/relative-url/relative-url.pipe.ts index 8eab3eb0b..499345d3c 100644 --- a/frontend/src/app/shared/pipes/relative-url/relative-url.pipe.ts +++ b/frontend/src/app/shared/pipes/relative-url/relative-url.pipe.ts @@ -1,5 +1,5 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { StateService } from '../../../services/state.service'; +import { StateService } from '@app/services/state.service'; @Pipe({ name: 'relativeUrl' diff --git a/frontend/src/app/shared/regex.utils.ts b/frontend/src/app/shared/regex.utils.ts index a349e9633..4d3985f56 100644 --- a/frontend/src/app/shared/regex.utils.ts +++ b/frontend/src/app/shared/regex.utils.ts @@ -1,4 +1,4 @@ -import { Env } from '../services/state.service'; +import { Env } from '@app/services/state.service'; // all base58 characters export const BASE58_CHARS = `[a-km-zA-HJ-NP-Z1-9]`; @@ -313,20 +313,24 @@ export function getRegex(type: RegexType, network?: Network): RegExp { } regex += `)`; // End the non-capturing group break; - // Match a date in the format YYYY-MM-DD (optional: HH:MM) + // Match a date in the format YYYY-MM-DD (optional: HH:MM or HH:MM:SS) // [Testing Order]: any order is fine case `date`: regex += `(?:`; // Start a non-capturing group regex += `${NUMBER_CHARS}{4}`; // Exactly 4 digits regex += `[-/]`; // 1 instance of the symbol "-" or "/" - regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits + regex += `${NUMBER_CHARS}{1,2}`; // 1 or 2 digits regex += `[-/]`; // 1 instance of the symbol "-" or "/" - regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits + regex += `${NUMBER_CHARS}{1,2}`; // 1 or 2 digits regex += `(?:`; // Start a non-capturing group regex += ` `; // 1 instance of the symbol " " - regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits + regex += `${NUMBER_CHARS}{1,2}`; // 1 or 2 digits regex += `:`; // 1 instance of the symbol ":" - regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits + regex += `${NUMBER_CHARS}{1,2}`; // 1 or 2 digits + regex += `(?:`; // Start a non-capturing group for optional seconds + regex += `:`; // 1 instance of the symbol ":" + regex += `${NUMBER_CHARS}{1,2}`; // 1 or 2 digits + regex += `)?`; // End the non-capturing group regex += `)?`; // End the non-capturing group. This group appears 0 or 1 times regex += `)`; // End the non-capturing group break; diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 6221f397d..a855f11b5 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -6,118 +6,120 @@ import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, fa faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, faCircleXmark, faCalendarCheck } from '@fortawesome/free-solid-svg-icons'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; -import { MenuComponent } from '../components/menu/menu.component'; -import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component'; -import { VbytesPipe } from './pipes/bytes-pipe/vbytes.pipe'; -import { ShortenStringPipe } from './pipes/shorten-string-pipe/shorten-string.pipe'; -import { CeilPipe } from './pipes/math-ceil/math-ceil.pipe'; -import { Hex2asciiPipe } from './pipes/hex2ascii/hex2ascii.pipe'; -import { Decimal2HexPipe } from './pipes/decimal2hex/decimal2hex.pipe'; -import { FeeRoundingPipe } from './pipes/fee-rounding/fee-rounding.pipe'; -import { AsmStylerPipe } from './pipes/asm-styler/asm-styler.pipe'; -import { AbsolutePipe } from './pipes/absolute/absolute.pipe'; -import { RelativeUrlPipe } from './pipes/relative-url/relative-url.pipe'; -import { ScriptpubkeyTypePipe } from './pipes/scriptpubkey-type-pipe/scriptpubkey-type.pipe'; -import { BytesPipe } from './pipes/bytes-pipe/bytes.pipe'; -import { WuBytesPipe } from './pipes/bytes-pipe/wubytes.pipe'; -import { FiatCurrencyPipe } from './pipes/fiat-currency.pipe'; -import { HttpErrorPipe } from './pipes/http-error-pipe/http-error.pipe'; -import { BlockchainComponent } from '../components/blockchain/blockchain.component'; -import { TimeComponent } from '../components/time/time.component'; -import { ClipboardComponent } from '../components/clipboard/clipboard.component'; -import { QrcodeComponent } from '../components/qrcode/qrcode.component'; -import { FiatComponent } from '../fiat/fiat.component'; +import { MenuComponent } from '@components/menu/menu.component'; +import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component'; +import { VbytesPipe } from '@app/shared/pipes/bytes-pipe/vbytes.pipe'; +import { ShortenStringPipe } from '@app/shared/pipes/shorten-string-pipe/shorten-string.pipe'; +import { CeilPipe } from '@app/shared/pipes/math-ceil/math-ceil.pipe'; +import { Hex2asciiPipe } from '@app/shared/pipes/hex2ascii/hex2ascii.pipe'; +import { Decimal2HexPipe } from '@app/shared/pipes/decimal2hex/decimal2hex.pipe'; +import { FeeRoundingPipe } from '@app/shared/pipes/fee-rounding/fee-rounding.pipe'; +import { AsmStylerPipe } from '@app/shared/pipes/asm-styler/asm-styler.pipe'; +import { AbsolutePipe } from '@app/shared/pipes/absolute/absolute.pipe'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; +import { ScriptpubkeyTypePipe } from '@app/shared/pipes/scriptpubkey-type-pipe/scriptpubkey-type.pipe'; +import { BytesPipe } from '@app/shared/pipes/bytes-pipe/bytes.pipe'; +import { WuBytesPipe } from '@app/shared/pipes/bytes-pipe/wubytes.pipe'; +import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe'; +import { HttpErrorPipe } from '@app/shared/pipes/http-error-pipe/http-error.pipe'; +import { BlockchainComponent } from '@components/blockchain/blockchain.component'; +import { TimeComponent } from '@components/time/time.component'; +import { ClipboardComponent } from '@components/clipboard/clipboard.component'; +import { QrcodeComponent } from '@components/qrcode/qrcode.component'; +import { FiatComponent } from '@app/fiat/fiat.component'; import { NgbNavModule, NgbTooltipModule, NgbPaginationModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; -import { TxFeaturesComponent } from '../components/tx-features/tx-features.component'; -import { TxFeeRatingComponent } from '../components/tx-fee-rating/tx-fee-rating.component'; +import { TxFeaturesComponent } from '@components/tx-features/tx-features.component'; +import { TxFeeRatingComponent } from '@components/tx-fee-rating/tx-fee-rating.component'; import { ReactiveFormsModule } from '@angular/forms'; -import { LanguageSelectorComponent } from '../components/language-selector/language-selector.component'; -import { FiatSelectorComponent } from '../components/fiat-selector/fiat-selector.component'; -import { RateUnitSelectorComponent } from '../components/rate-unit-selector/rate-unit-selector.component'; -import { ThemeSelectorComponent } from '../components/theme-selector/theme-selector.component'; -import { BrowserOnlyDirective } from './directives/browser-only.directive'; -import { ServerOnlyDirective } from './directives/server-only.directive'; -import { ColoredPriceDirective } from './directives/colored-price.directive'; -import { NoSanitizePipe } from './pipes/no-sanitize.pipe'; -import { MempoolBlocksComponent } from '../components/mempool-blocks/mempool-blocks.component'; -import { BlockchainBlocksComponent } from '../components/blockchain-blocks/blockchain-blocks.component'; -import { AmountComponent } from '../components/amount/amount.component'; +import { LanguageSelectorComponent } from '@components/language-selector/language-selector.component'; +import { FiatSelectorComponent } from '@components/fiat-selector/fiat-selector.component'; +import { RateUnitSelectorComponent } from '@components/rate-unit-selector/rate-unit-selector.component'; +import { ThemeSelectorComponent } from '@components/theme-selector/theme-selector.component'; +import { AmountSelectorComponent } from '@components/amount-selector/amount-selector.component'; +import { BrowserOnlyDirective } from '@app/shared/directives/browser-only.directive'; +import { ServerOnlyDirective } from '@app/shared/directives/server-only.directive'; +import { ColoredPriceDirective } from '@app/shared/directives/colored-price.directive'; +import { NoSanitizePipe } from '@app/shared/pipes/no-sanitize.pipe'; +import { MempoolBlocksComponent } from '@components/mempool-blocks/mempool-blocks.component'; +import { BlockchainBlocksComponent } from '@components/blockchain-blocks/blockchain-blocks.component'; +import { AmountComponent } from '@components/amount/amount.component'; import { RouterModule } from '@angular/router'; -import { CapAddressPipe } from './pipes/cap-address-pipe/cap-address-pipe'; -import { StartComponent } from '../components/start/start.component'; -import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component'; -import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component'; -import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component'; -import { BlockFiltersComponent } from '../components/block-filters/block-filters.component'; -import { AddressGroupComponent } from '../components/address-group/address-group.component'; -import { SearchFormComponent } from '../components/search-form/search-form.component'; -import { AddressLabelsComponent } from '../components/address-labels/address-labels.component'; -import { FooterComponent } from '../components/footer/footer.component'; -import { AssetComponent } from '../components/asset/asset.component'; -import { AssetsComponent } from '../components/assets/assets.component'; -import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component'; -import { StatusViewComponent } from '../components/status-view/status-view.component'; -import { ServerHealthComponent } from '../components/server-health/server-health.component'; -import { ServerStatusComponent } from '../components/server-health/server-status.component'; -import { FeesBoxComponent } from '../components/fees-box/fees-box.component'; -import { DifficultyComponent } from '../components/difficulty/difficulty.component'; -import { DifficultyTooltipComponent } from '../components/difficulty/difficulty-tooltip.component'; -import { DifficultyMiningComponent } from '../components/difficulty-mining/difficulty-mining.component'; -import { BalanceWidgetComponent } from '../components/balance-widget/balance-widget.component'; -import { AddressTransactionsWidgetComponent } from '../components/address-transactions-widget/address-transactions-widget.component'; -import { RbfTimelineComponent } from '../components/rbf-timeline/rbf-timeline.component'; -import { AccelerationTimelineComponent } from '../components/acceleration-timeline/acceleration-timeline.component'; -import { RbfTimelineTooltipComponent } from '../components/rbf-timeline/rbf-timeline-tooltip.component'; -import { AccelerationTimelineTooltipComponent } from '../components/acceleration-timeline/acceleration-timeline-tooltip.component'; -import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component'; -import { TestTransactionsComponent } from '../components/test-transactions/test-transactions.component'; -import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component'; -import { AssetGroupComponent } from '../components/assets/asset-group/asset-group.component'; -import { AssetCirculationComponent } from '../components/asset-circulation/asset-circulation.component'; -import { AmountShortenerPipe } from '../shared/pipes/amount-shortener.pipe'; -import { DifficultyAdjustmentsTable } from '../components/difficulty-adjustments-table/difficulty-adjustments-table.components'; -import { BlocksList } from '../components/blocks-list/blocks-list.component'; -import { RbfList } from '../components/rbf-list/rbf-list.component'; -import { RewardStatsComponent } from '../components/reward-stats/reward-stats.component'; -import { DataCyDirective } from '../data-cy.directive'; -import { LoadingIndicatorComponent } from '../components/loading-indicator/loading-indicator.component'; -import { IndexingProgressComponent } from '../components/indexing-progress/indexing-progress.component'; -import { SvgImagesComponent } from '../components/svg-images/svg-images.component'; -import { ChangeComponent } from '../components/change/change.component'; -import { SatsComponent } from './components/sats/sats.component'; -import { BtcComponent } from './components/btc/btc.component'; -import { FeeRateComponent } from './components/fee-rate/fee-rate.component'; -import { AddressTypeComponent } from './components/address-type/address-type.component'; -import { TruncateComponent } from './components/truncate/truncate.component'; -import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component'; -import { TimestampComponent } from './components/timestamp/timestamp.component'; -import { ConfirmationsComponent } from './components/confirmations/confirmations.component'; -import { ToggleComponent } from './components/toggle/toggle.component'; -import { GeolocationComponent } from '../shared/components/geolocation/geolocation.component'; -import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.component'; -import { GlobalFooterComponent } from './components/global-footer/global-footer.component'; -import { MempoolErrorComponent } from './components/mempool-error/mempool-error.component'; -import { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component'; -import { PendingStatsComponent } from '../components/acceleration/pending-stats/pending-stats.component'; -import { AccelerationStatsComponent } from '../components/acceleration/acceleration-stats/acceleration-stats.component'; -import { AccelerationSparklesComponent } from '../components/acceleration/sparkles/acceleration-sparkles.component'; +import { CapAddressPipe } from '@app/shared/pipes/cap-address-pipe/cap-address-pipe'; +import { StartComponent } from '@components/start/start.component'; +import { TransactionsListComponent } from '@components/transactions-list/transactions-list.component'; +import { BlockOverviewGraphComponent } from '@components/block-overview-graph/block-overview-graph.component'; +import { BlockOverviewTooltipComponent } from '@components/block-overview-tooltip/block-overview-tooltip.component'; +import { BlockFiltersComponent } from '@components/block-filters/block-filters.component'; +import { AddressGroupComponent } from '@components/address-group/address-group.component'; +import { SearchFormComponent } from '@components/search-form/search-form.component'; +import { AddressLabelsComponent } from '@components/address-labels/address-labels.component'; +import { FooterComponent } from '@components/footer/footer.component'; +import { AssetComponent } from '@components/asset/asset.component'; +import { AssetsComponent } from '@components/assets/assets.component'; +import { AssetsNavComponent } from '@components/assets/assets-nav/assets-nav.component'; +import { StatusViewComponent } from '@components/status-view/status-view.component'; +import { ServerHealthComponent } from '@components/server-health/server-health.component'; +import { ServerStatusComponent } from '@components/server-health/server-status.component'; +import { FeesBoxComponent } from '@components/fees-box/fees-box.component'; +import { DifficultyComponent } from '@components/difficulty/difficulty.component'; +import { DifficultyTooltipComponent } from '@components/difficulty/difficulty-tooltip.component'; +import { DifficultyMiningComponent } from '@components/difficulty-mining/difficulty-mining.component'; +import { BalanceWidgetComponent } from '@components/balance-widget/balance-widget.component'; +import { AddressTransactionsWidgetComponent } from '@components/address-transactions-widget/address-transactions-widget.component'; +import { RbfTimelineComponent } from '@components/rbf-timeline/rbf-timeline.component'; +import { AccelerationTimelineComponent } from '@components/acceleration-timeline/acceleration-timeline.component'; +import { RbfTimelineTooltipComponent } from '@components/rbf-timeline/rbf-timeline-tooltip.component'; +import { AccelerationTimelineTooltipComponent } from '@components/acceleration-timeline/acceleration-timeline-tooltip.component'; +import { PushTransactionComponent } from '@components/push-transaction/push-transaction.component'; +import { TestTransactionsComponent } from '@components/test-transactions/test-transactions.component'; +import { AssetsFeaturedComponent } from '@components/assets/assets-featured/assets-featured.component'; +import { AssetGroupComponent } from '@components/assets/asset-group/asset-group.component'; +import { AssetCirculationComponent } from '@components/asset-circulation/asset-circulation.component'; +import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe'; +import { DifficultyAdjustmentsTable } from '@components/difficulty-adjustments-table/difficulty-adjustments-table.components'; +import { BlocksList } from '@components/blocks-list/blocks-list.component'; +import { RbfList } from '@components/rbf-list/rbf-list.component'; +import { RewardStatsComponent } from '@components/reward-stats/reward-stats.component'; +import { DataCyDirective } from '@app/data-cy.directive'; +import { LoadingIndicatorComponent } from '@components/loading-indicator/loading-indicator.component'; +import { IndexingProgressComponent } from '@components/indexing-progress/indexing-progress.component'; +import { SvgImagesComponent } from '@components/svg-images/svg-images.component'; +import { ChangeComponent } from '@components/change/change.component'; +import { SatsComponent } from '@app/shared/components/sats/sats.component'; +import { BtcComponent } from '@app/shared/components/btc/btc.component'; +import { FeeRateComponent } from '@app/shared/components/fee-rate/fee-rate.component'; +import { AddressTypeComponent } from '@app/shared/components/address-type/address-type.component'; +import { TruncateComponent } from '@app/shared/components/truncate/truncate.component'; +import { SearchResultsComponent } from '@components/search-form/search-results/search-results.component'; +import { TimestampComponent } from '@app/shared/components/timestamp/timestamp.component'; +import { ConfirmationsComponent } from '@app/shared/components/confirmations/confirmations.component'; +import { ToggleComponent } from '@app/shared/components/toggle/toggle.component'; +import { GeolocationComponent } from '@app/shared/components/geolocation/geolocation.component'; +import { TestnetAlertComponent } from '@app/shared/components/testnet-alert/testnet-alert.component'; +import { GlobalFooterComponent } from '@app/shared/components/global-footer/global-footer.component'; +import { MempoolErrorComponent } from '@app/shared/components/mempool-error/mempool-error.component'; +import { AccelerationsListComponent } from '@components/acceleration/accelerations-list/accelerations-list.component'; +import { PendingStatsComponent } from '@components/acceleration/pending-stats/pending-stats.component'; +import { AccelerationStatsComponent } from '@components/acceleration/acceleration-stats/acceleration-stats.component'; +import { AccelerationSparklesComponent } from '@components/acceleration/sparkles/acceleration-sparkles.component'; +import { OrdDataComponent } from '@components/ord-data/ord-data.component'; -import { BlockViewComponent } from '../components/block-view/block-view.component'; -import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component'; -import { MempoolBlockViewComponent } from '../components/mempool-block-view/mempool-block-view.component'; -import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component'; -import { ClockchainComponent } from '../components/clockchain/clockchain.component'; -import { ClockFaceComponent } from '../components/clock-face/clock-face.component'; -import { ClockComponent } from '../components/clock/clock.component'; -import { CalculatorComponent } from '../components/calculator/calculator.component'; -import { BitcoinsatoshisPipe } from '../shared/pipes/bitcoinsatoshis.pipe'; -import { HttpErrorComponent } from '../shared/components/http-error/http-error.component'; -import { TwitterWidgetComponent } from '../components/twitter-widget/twitter-widget.component'; -import { FaucetComponent } from '../components/faucet/faucet.component'; -import { TwitterLogin } from '../components/twitter-login/twitter-login.component'; -import { BitcoinInvoiceComponent } from '../components/bitcoin-invoice/bitcoin-invoice.component'; +import { BlockViewComponent } from '@components/block-view/block-view.component'; +import { EightBlocksComponent } from '@components/eight-blocks/eight-blocks.component'; +import { MempoolBlockViewComponent } from '@components/mempool-block-view/mempool-block-view.component'; +import { MempoolBlockOverviewComponent } from '@components/mempool-block-overview/mempool-block-overview.component'; +import { ClockchainComponent } from '@components/clockchain/clockchain.component'; +import { ClockFaceComponent } from '@components/clock-face/clock-face.component'; +import { ClockComponent } from '@components/clock/clock.component'; +import { CalculatorComponent } from '@components/calculator/calculator.component'; +import { BitcoinsatoshisPipe } from '@app/shared/pipes/bitcoinsatoshis.pipe'; +import { HttpErrorComponent } from '@app/shared/components/http-error/http-error.component'; +import { TwitterWidgetComponent } from '@components/twitter-widget/twitter-widget.component'; +import { FaucetComponent } from '@components/faucet/faucet.component'; +import { TwitterLogin } from '@components/twitter-login/twitter-login.component'; +import { BitcoinInvoiceComponent } from '@components/bitcoin-invoice/bitcoin-invoice.component'; -import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-directives/weight-directives'; +import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/weight-directives/weight-directives'; @NgModule({ declarations: [ @@ -131,6 +133,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir FiatSelectorComponent, ThemeSelectorComponent, RateUnitSelectorComponent, + AmountSelectorComponent, ScriptpubkeyTypePipe, RelativeUrlPipe, NoSanitizePipe, @@ -227,6 +230,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir AccelerationStatsComponent, PendingStatsComponent, AccelerationSparklesComponent, + OrdDataComponent, HttpErrorComponent, TwitterWidgetComponent, FaucetComponent, @@ -278,6 +282,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir FiatSelectorComponent, RateUnitSelectorComponent, ThemeSelectorComponent, + AmountSelectorComponent, ScriptpubkeyTypePipe, RelativeUrlPipe, Hex2asciiPipe, @@ -358,10 +363,12 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir AccelerationStatsComponent, PendingStatsComponent, AccelerationSparklesComponent, + OrdDataComponent, HttpErrorComponent, TwitterWidgetComponent, TwitterLogin, BitcoinInvoiceComponent, + BitcoinsatoshisPipe, MempoolBlockOverviewComponent, ClockchainComponent, diff --git a/frontend/src/app/shared/transaction.utils.ts b/frontend/src/app/shared/transaction.utils.ts index bbf28a250..b3678986b 100644 --- a/frontend/src/app/shared/transaction.utils.ts +++ b/frontend/src/app/shared/transaction.utils.ts @@ -1,8 +1,8 @@ -import { TransactionFlags } from './filters.utils'; -import { getVarIntLength, opcodes, parseMultisigScript, isPoint } from './script.utils'; -import { Transaction } from '../interfaces/electrs.interface'; -import { CpfpInfo, RbfInfo, TransactionStripped } from '../interfaces/node-api.interface'; -import { StateService } from '../services/state.service'; +import { TransactionFlags } from '@app/shared/filters.utils'; +import { getVarIntLength, opcodes, parseMultisigScript, isPoint } from '@app/shared/script.utils'; +import { Transaction } from '@interfaces/electrs.interface'; +import { CpfpInfo, RbfInfo, TransactionStripped } from '@interfaces/node-api.interface'; +import { StateService } from '@app/services/state.service'; // Bitcoin Core default policy settings const MAX_STANDARD_TX_WEIGHT = 400_000; @@ -587,4 +587,4 @@ export function identifyPrioritizedTransactions(transactions: TransactionStrippe } return { prioritized, deprioritized }; -} \ No newline at end of file +} diff --git a/frontend/src/index.mempool.bitb.html b/frontend/src/index.mempool.bitb.html new file mode 100644 index 000000000..a886e5ec2 --- /dev/null +++ b/frontend/src/index.mempool.bitb.html @@ -0,0 +1,45 @@ + + + + + + BITB | Bitwise Bitcoin ETF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/resources/bitb/bitb-preview.png b/frontend/src/resources/bitb/bitb-preview.png new file mode 100644 index 000000000..da2aba024 Binary files /dev/null and b/frontend/src/resources/bitb/bitb-preview.png differ diff --git a/frontend/src/resources/bitb/favicons/android-chrome-192x192.png b/frontend/src/resources/bitb/favicons/android-chrome-192x192.png new file mode 100644 index 000000000..824630012 Binary files /dev/null and b/frontend/src/resources/bitb/favicons/android-chrome-192x192.png differ diff --git a/frontend/src/resources/bitb/favicons/android-chrome-512x512.png b/frontend/src/resources/bitb/favicons/android-chrome-512x512.png new file mode 100644 index 000000000..5a6ed20f9 Binary files /dev/null and b/frontend/src/resources/bitb/favicons/android-chrome-512x512.png differ diff --git a/frontend/src/resources/bitb/favicons/apple-touch-icon.png b/frontend/src/resources/bitb/favicons/apple-touch-icon.png new file mode 100644 index 000000000..e93df1f49 Binary files /dev/null and b/frontend/src/resources/bitb/favicons/apple-touch-icon.png differ diff --git a/frontend/src/resources/bitb/favicons/favicon-16x16.png b/frontend/src/resources/bitb/favicons/favicon-16x16.png new file mode 100644 index 000000000..336dcda94 Binary files /dev/null and b/frontend/src/resources/bitb/favicons/favicon-16x16.png differ diff --git a/frontend/src/resources/bitb/favicons/favicon-32x32.png b/frontend/src/resources/bitb/favicons/favicon-32x32.png new file mode 100644 index 000000000..ad5589e65 Binary files /dev/null and b/frontend/src/resources/bitb/favicons/favicon-32x32.png differ diff --git a/frontend/src/resources/bitb/favicons/favicon.ico b/frontend/src/resources/bitb/favicons/favicon.ico new file mode 100644 index 000000000..9c72cced0 Binary files /dev/null and b/frontend/src/resources/bitb/favicons/favicon.ico differ diff --git a/frontend/src/resources/bitb/favicons/site.webmanifest b/frontend/src/resources/bitb/favicons/site.webmanifest new file mode 100644 index 000000000..45dc8a206 --- /dev/null +++ b/frontend/src/resources/bitb/favicons/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/frontend/src/resources/bitblogo.svg b/frontend/src/resources/bitblogo.svg new file mode 100644 index 000000000..6c69a2a4c --- /dev/null +++ b/frontend/src/resources/bitblogo.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/resources/profile/wizardhat.png b/frontend/src/resources/profile/wizardhat.png new file mode 100644 index 000000000..d385a56fa Binary files /dev/null and b/frontend/src/resources/profile/wizardhat.png differ diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 44795bd55..df2bfb201 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -2,7 +2,13 @@ "extends": "./tsconfig.base.json", "compilerOptions": { "outDir": "./out-tsc/app", - "types": [] + "types": [], + "paths": { + "@app/*" : ["src/app/*"], + "@components/*" : ["src/app/components/*"], + "@environments/*" : ["src/environments/*"], + "@interfaces/*" : ["src/app/interfaces/*"] + } }, "files": [ "src/main.ts", diff --git a/production/bitcoin.crontab b/production/bitcoin.crontab index d1e484a0b..a5bc64241 100644 --- a/production/bitcoin.crontab +++ b/production/bitcoin.crontab @@ -1,4 +1,5 @@ @reboot sleep 5 ; /usr/local/bin/bitcoind -testnet >/dev/null 2>&1 +@reboot sleep 5 ; /usr/local/bin/bitcoind -testnet4 >/dev/null 2>&1 @reboot sleep 5 ; /usr/local/bin/bitcoind -signet >/dev/null 2>&1 @reboot sleep 10 ; screen -dmS mainnet /bitcoin/electrs/start mainnet @reboot sleep 10 ; screen -dmS testnet /bitcoin/electrs/start testnet diff --git a/production/install b/production/install index bf7153557..1cac9e8e9 100755 --- a/production/install +++ b/production/install @@ -47,6 +47,7 @@ UNFURL_INSTALL=ON BITCOIN_MAINNET_ENABLE=ON BITCOIN_MAINNET_MINFEE_ENABLE=ON BITCOIN_TESTNET_ENABLE=ON +BITCOIN_TESTNET4_ENABLE=ON BITCOIN_SIGNET_ENABLE=ON BITCOIN_MAINNET_LIGHTNING_ENABLE=ON BITCOIN_TESTNET_LIGHTNING_ENABLE=ON @@ -100,6 +101,13 @@ BITCOIN_TESTNET_P2P_PORT=18333 BITCOIN_TESTNET_RPC_HOST=127.0.0.1 BITCOIN_TESTNET_RPC_PORT=18332 +# used for firewall configuration +BITCOIN_TESTNET4_P2P_HOST=127.0.0.1 +BITCOIN_TESTNET4_P2P_PORT=48333 +# used for RPC communication +BITCOIN_TESTNET4_RPC_HOST=127.0.0.1 +BITCOIN_TESTNET4_RPC_PORT=48332 + # used for firewall configuration BITCOIN_SIGNET_P2P_HOST=127.0.0.1 BITCOIN_SIGNET_P2P_PORT=18333 @@ -139,6 +147,11 @@ ELECTRS_LIQUID_HTTP_PORT=3001 ELECTRS_TESTNET_HTTP_HOST=127.0.0.1 ELECTRS_TESTNET_HTTP_PORT=3002 +# set either socket or TCP host/port, not both +#ELECTRS_TESTNET4_HTTP_SOCK=/tmp/bitcoin.testnet4.electrs +ELECTRS_TESTNET4_HTTP_HOST=127.0.0.1 +ELECTRS_TESTNET4_HTTP_PORT=3005 + # set either socket or TCP host/port, not both #ELECTRS_SIGNET_HTTP_SOCK=/tmp/bitcoin.testnet.electrs ELECTRS_SIGNET_HTTP_HOST=127.0.0.1 @@ -164,6 +177,11 @@ MEMPOOL_LIQUID_HTTP_PORT=8998 MEMPOOL_TESTNET_HTTP_HOST=127.0.0.1 MEMPOOL_TESTNET_HTTP_PORT=8997 +# set either socket or TCP host/port, not both +#MEMPOOL_TESTNET4_HTTP_SOCK=/tmp/bitcoin.testnet.mempool +MEMPOOL_TESTNET4_HTTP_HOST=127.0.0.1 +MEMPOOL_TESTNET4_HTTP_PORT=8990 + # set either socket or TCP host/port, not both #MEMPOOL_BISQ_HTTP_SOCK=/tmp/bitcoin.bisq.mempool MEMPOOL_BISQ_HTTP_HOST=127.0.0.1 @@ -231,6 +249,7 @@ MYSQL_GROUP=mysql # mempool mysql user/password MEMPOOL_MAINNET_USER='mempool' MEMPOOL_TESTNET_USER='mempool_testnet' +MEMPOOL_TESTNET4_USER='mempool_testnet4' MEMPOOL_SIGNET_USER='mempool_signet' MEMPOOL_MAINNET_LIGHTNING_USER='mempool_mainnet_lightning' MEMPOOL_TESTNET_LIGHTNING_USER='mempool_testnet_lightning' @@ -241,6 +260,7 @@ MEMPOOL_BISQ_USER='mempool_bisq' # generate random hex string MEMPOOL_MAINNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}') MEMPOOL_TESTNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}') +MEMPOOL_TESTNET4_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}') MEMPOOL_SIGNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}') MEMPOOL_MAINNET_LIGHTNING_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}') MEMPOOL_TESTNET_LIGHTNING_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}') @@ -265,7 +285,9 @@ BITCOIN_HOME=/bitcoin # bitcoin testnet data BITCOIN_TESTNET_DATA=${BITCOIN_HOME}/testnet3 -# bitcoin testnet data +# bitcoin testnet4 data +BITCOIN_TESTNET4_DATA=${BITCOIN_HOME}/testnet4 +# bitcoin signet data BITCOIN_SIGNET_DATA=${BITCOIN_HOME}/signet # bitcoin electrs source/binaries @@ -279,6 +301,9 @@ ELECTRS_MAINNET_DATA=${ELECTRS_DATA_ROOT}/mainnet # bitcoin testnet electrs database, only a few GB ELECTRS_TESTNET_ZPOOL=${ZPOOL} ELECTRS_TESTNET_DATA=${ELECTRS_DATA_ROOT}/testnet +# bitcoin testnet4 electrs database, only a few GB +ELECTRS_TESTNET4_ZPOOL=${ZPOOL} +ELECTRS_TESTNET4_DATA=${ELECTRS_DATA_ROOT}/testnet4 # bitcoin signet electrs database, only a few GB ELECTRS_SIGNET_ZPOOL=${ZPOOL} ELECTRS_SIGNET_DATA=${ELECTRS_DATA_ROOT}/signet @@ -332,7 +357,7 @@ BITCOIN_REPO_URL=https://github.com/bitcoin/bitcoin BITCOIN_REPO_NAME=bitcoin BITCOIN_REPO_BRANCH=master #BITCOIN_LATEST_RELEASE=$(curl -s https://api.github.com/repos/bitcoin/bitcoin/releases/latest|grep tag_name|head -1|cut -d '"' -f4) -BITCOIN_LATEST_RELEASE=v25.1 +BITCOIN_LATEST_RELEASE=v28.0 echo -n '.' BISQ_REPO_URL=https://github.com/bisq-network/bisq @@ -392,7 +417,7 @@ DEBIAN_UNFURL_PKG+=(libxdamage-dev libxrandr-dev libgbm-dev libpango1.0-dev liba # packages needed for mempool ecosystem FREEBSD_PKG=() FREEBSD_PKG+=(zsh sudo git git-lfs screen curl wget calc neovim) -FREEBSD_PKG+=(openssh-portable py311-pip rust llvm18 jq base64 libzmq4) +FREEBSD_PKG+=(openssh-portable py311-pip rust llvm17 jq base64 libzmq4) FREEBSD_PKG+=(boost-libs autoconf automake gmake gcc libevent libtool pkgconf) FREEBSD_PKG+=(nginx rsync py311-certbot-nginx mariadb1011-server) FREEBSD_PKG+=(geoipupdate redis) @@ -567,6 +592,15 @@ zfsCreateFilesystems() done fi + # Bitcoin Testnet4 + if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then + zfs create -o "mountpoint=${BITCOIN_TESTNET4_DATA}" "${ZPOOL}/bitcoin/testnet4" + for folder in chainstate indexes blocks + do + zfs create -o "mountpoint=${BITCOIN_TESTNET4_DATA}/${folder}" "${ZPOOL}/bitcoin/testnet4/${folder}" + done + fi + # Bitcoin Signet if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then zfs create -o "mountpoint=${BITCOIN_SIGNET_DATA}" "${ZPOOL}/bitcoin/signet" @@ -594,6 +628,15 @@ zfsCreateFilesystems() done fi + # electrs testnet4 data + if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then + zfs create -o "mountpoint=${ELECTRS_TESTNET4_DATA}" "${ELECTRS_TESTNET4_ZPOOL}/electrs/testnet4" + for folder in cache history txstore + do + zfs create -o "mountpoint=${ELECTRS_TESTNET4_DATA}/newindex/${folder}" "${ELECTRS_TESTNET4_ZPOOL}/electrs/testnet4/${folder}" + done + fi + # electrs signet data if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then zfs create -o "mountpoint=${ELECTRS_SIGNET_DATA}" "${ELECTRS_SIGNET_ZPOOL}/electrs/signet" @@ -651,6 +694,15 @@ ext4CreateDir() done fi + # Bitcoin Testnet4 + if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then + mkdir -p "${BITCOIN_TESTNET4_DATA}" + for folder in chainstate indexes blocks + do + mkdir -p "${BITCOIN_TESTNET4_DATA}/${folder}" + done + fi + # Bitcoin Signet if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then mkdir -p "${BITCOIN_SIGNET_DATA}" @@ -678,6 +730,15 @@ ext4CreateDir() done fi + # electrs testnet4 data + if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then + mkdir -p "${ELECTRS_TESTNET4_DATA}" + for folder in cache history txstore + do + mkdir -p "${ELECTRS_TESTNET4_DATA}/newindex/${folder}" + done + fi + # electrs signet data if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then mkdir -p "${ELECTRS_SIGNET_DATA}" @@ -769,6 +830,7 @@ LN-Mainnet:Enable Bitcoin Mainnet Lightning:ON LN-Testnet:Enable Bitcoin Testnet Lightning:ON LN-Signet:Enable Bitcoin Signet Lightning:ON Testnet:Enable Bitcoin Testnet:ON +Testnet4:Enable Bitcoin Testnet4:ON Signet:Enable Bitcoin Signet:ON Liquid:Enable Elements Liquid:ON Liquidtestnet:Enable Elements Liquidtestnet:ON @@ -818,13 +880,19 @@ else BITCOIN_TESTNET_ENABLE=OFF fi +if grep Testnet4 $tempfile >/dev/null 2>&1;then + BITCOIN_TESTNET4_ENABLE=ON +else + BITCOIN_TESTNET4_ENABLE=OFF +fi + if grep Signet $tempfile >/dev/null 2>&1;then BITCOIN_SIGNET_ENABLE=ON else BITCOIN_SIGNET_ENABLE=OFF fi -if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_MAINNET_MINFEE_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then +if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_MAINNET_MINFEE_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_TESTNET4_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then BITCOIN_INSTALL=ON else BITCOIN_INSTALL=OFF @@ -872,7 +940,7 @@ else CLN_INSTALL=OFF fi -if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then +if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_TESTNET4_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then BITCOIN_ELECTRS_INSTALL=ON else BITCOIN_ELECTRS_INSTALL=OFF @@ -1216,6 +1284,9 @@ if [ "${BITCOIN_ELECTRS_INSTALL}" = ON ];then if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_TESTNET_DATA}" fi + if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then + osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_TESTNET4_DATA}" + fi if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_SIGNET_DATA}" fi @@ -1520,7 +1591,7 @@ fi # Bitcoin instance for Mainnet Minfee # ####################################### -if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then +if [ "${BITCOIN_MAINNET_MINFEE_ENABLE}" = ON ];then echo "[*] Installing Bitcoin Minfee service" case $OS in @@ -1550,6 +1621,23 @@ if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then esac fi +################################# +# Bitcoin instance for Testnet4 # +################################# + +if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then + echo "[*] Installing Bitcoin Testnet service" + case $OS in + + FreeBSD) + ;; + + Debian) + osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/linux/bitcoin-testnet4.service" "${DEBIAN_SERVICE_HOME}" + ;; + esac +fi + ############################### # Bitcoin instance for Signet # ############################### @@ -1616,6 +1704,14 @@ if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then echo "[*] FIXME: must only crontab enabled daemons" fi +######################################### +# Electrs instance for Bitcoin Testnet4 # +######################################### + +if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then + echo "[*] FIXME: must only crontab enabled daemons" +fi + ####################################### # Electrs instance for Bitcoin Signet # ####################################### @@ -1668,11 +1764,15 @@ case $OS in echo "[*] Installing Electrs Testnet Cronjob" crontab_bitcoin+="@reboot sleep 70 ; screen -dmS testnet /bitcoin/electrs/start testnet\n" fi + if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then + echo "[*] Installing Electrs Testnet4 Cronjob" + crontab_bitcoin+="@reboot sleep 110 ; screen -dmS testnet4 /bitcoin/electrs/start testnet4\n" + fi if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then echo "[*] Installing Electrs Signet Cronjob" crontab_bitcoin+="@reboot sleep 90 ; screen -dmS signet /bitcoin/electrs/start signet\n" fi - if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then + if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_TESTNET4_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then echo "${crontab_bitcoin}" | crontab -u "${BITCOIN_USER}" - fi @@ -1700,7 +1800,7 @@ fi ##### Mempool -> Bitcoin Mainnet instance -if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then +if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_TESTNET4_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then echo "[*] Creating Mempool instance for Bitcoin Mainnet" osSudo "${MEMPOOL_USER}" git config --global advice.detachedHead false osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/mainnet" @@ -1727,6 +1827,15 @@ if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/testnet && git checkout ${MEMPOOL_LATEST_RELEASE}" fi +if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then + echo "[*] Creating Mempool instance for Bitcoin Testnet4" + osSudo "${MEMPOOL_USER}" git config --global advice.detachedHead false + osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/testnet4" + + echo "[*] Checking out Mempool ${MEMPOOL_LATEST_RELEASE} for Bitcoin Testnet4" + osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/testnet4 && git checkout ${MEMPOOL_LATEST_RELEASE}" +fi + if [ "${BITCOIN_TESTNET_LIGHTNING_ENABLE}" = ON ];then echo "[*] Creating Mempool instance for Lightning Network on Bitcoin Testnet" osSudo "${MEMPOOL_USER}" git config --global advice.detachedHead false @@ -1804,6 +1913,9 @@ grant all on mempool.* to '${MEMPOOL_MAINNET_USER}'@'localhost' identified by '$ create database mempool_testnet; grant all on mempool_testnet.* to '${MEMPOOL_TESTNET_USER}'@'localhost' identified by '${MEMPOOL_TESTNET_PASS}'; +create database mempool_testnet4; +grant all on mempool_testnet4.* to '${MEMPOOL_TESTNET4_USER}'@'localhost' identified by '${MEMPOOL_TESTNET4_PASS}'; + create database mempool_signet; grant all on mempool_signet.* to '${MEMPOOL_SIGNET_USER}'@'localhost' identified by '${MEMPOOL_SIGNET_PASS}'; @@ -1832,6 +1944,8 @@ declare -x MEMPOOL_MAINNET_USER="${MEMPOOL_MAINNET_USER}" declare -x MEMPOOL_MAINNET_PASS="${MEMPOOL_MAINNET_PASS}" declare -x MEMPOOL_TESTNET_USER="${MEMPOOL_TESTNET_USER}" declare -x MEMPOOL_TESTNET_PASS="${MEMPOOL_TESTNET_PASS}" +declare -x MEMPOOL_TESTNET4_USER="${MEMPOOL_TESTNET4_USER}" +declare -x MEMPOOL_TESTNET4_PASS="${MEMPOOL_TESTNET4_PASS}" declare -x MEMPOOL_SIGNET_USER="${MEMPOOL_SIGNET_USER}" declare -x MEMPOOL_SIGNET_PASS="${MEMPOOL_SIGNET_PASS}" declare -x MEMPOOL_MAINNET_LIGHTNING_USER="${MEMPOOL_MAINNET_LIGHTNING_USER}" @@ -1932,6 +2046,9 @@ EOF if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then osSudo "${ROOT_USER}" systemctl enable bitcoin-testnet.service fi + if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then + osSudo "${ROOT_USER}" systemctl enable bitcoin-testnet4.service + fi if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then osSudo "${ROOT_USER}" systemctl enable bitcoin-signet.service fi diff --git a/production/linux/bitcoin-testnet4.service b/production/linux/bitcoin-testnet4.service new file mode 100644 index 000000000..1bb893c44 --- /dev/null +++ b/production/linux/bitcoin-testnet4.service @@ -0,0 +1,22 @@ +[Unit] +Description=Bitcoind-testnet4 +After=network.target + +[Service] +ExecStart=/usr/local/bin/bitcoind -conf=bitcoin.conf -daemon -testnet4 -printtoconsole -pid=/bitcoin/bitcoind-testnet4.pid +ExecStop=/usr/local/bin/bitcoin-cli -testnet4 stop + +Type=forking +PIDFile=/bitcoin/bitcoind-testnet4.pid +Restart=on-failure + +User=bitcoin +Group=bitcoin + +PrivateTmp=true +ProtectSystem=full +NoNewPrivileges=true +PrivateDevices=true + +[Install] +WantedBy=multi-user.target diff --git a/production/mempool-config.mainnet.json b/production/mempool-config.mainnet.json index b6ee68da1..f57978043 100644 --- a/production/mempool-config.mainnet.json +++ b/production/mempool-config.mainnet.json @@ -150,5 +150,9 @@ "MEMPOOL_SERVICES": { "API": "https://mempool.space/api/v1/services", "ACCELERATIONS": true + }, + "WALLETS": { + "ENABLED": true, + "WALLETS": ["BITB"] } } diff --git a/production/mempool-frontend-config.mainnet.json b/production/mempool-frontend-config.mainnet.json index 84cde82cf..61a8c2c2a 100644 --- a/production/mempool-frontend-config.mainnet.json +++ b/production/mempool-frontend-config.mainnet.json @@ -10,6 +10,9 @@ "MEMPOOL_WEBSITE_URL": "https://mempool.space", "LIQUID_WEBSITE_URL": "https://liquid.network", "BISQ_WEBSITE_URL": "https://bisq.markets", + "MAINNET_BLOCK_AUDIT_START_HEIGHT": 773911, + "TESTNET_BLOCK_AUDIT_START_HEIGHT": 2417829, + "SIGNET_BLOCK_AUDIT_START_HEIGHT": 127609, "ITEMS_PER_PAGE": 25, "LIGHTNING": true, "ACCELERATOR": true, diff --git a/production/mempool-reset-all b/production/mempool-reset-all index 22f004610..d7e8ba249 100755 --- a/production/mempool-reset-all +++ b/production/mempool-reset-all @@ -1,3 +1,5 @@ #!/usr/bin/env zsh -rm $HOME/*/backend/mempool-config.json -rm $HOME/*/frontend/mempool-frontend-config.json +rm -f $HOME/*/backend/mempool-config.json +rm -f $HOME/*/frontend/mempool-frontend-config.json +rm -f $HOME/*/frontend/projects/mempool/mempool-frontend-config.json +exit 0 diff --git a/production/nginx/location-api-v1-services.conf b/production/nginx/location-api-v1-services.conf index 88f510e79..a9df64bc6 100644 --- a/production/nginx/location-api-v1-services.conf +++ b/production/nginx/location-api-v1-services.conf @@ -92,6 +92,7 @@ location @mempool-api-v1-services-cache-disabled-addcors { set $cors_methods 'GET, POST, PUT, DELETE, OPTIONS'; set $cors_origin 'https://mempool.space'; set $cors_headers 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With'; + set $cors_expose_headers 'X-Total-Count'; set $cors_credentials 'true'; # set CORS for approved hostnames @@ -100,6 +101,7 @@ location @mempool-api-v1-services-cache-disabled-addcors { set $cors_methods 'GET, POST, PUT, DELETE, OPTIONS'; set $cors_origin "$http_origin"; set $cors_headers 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With'; + set $cors_expose_headers 'X-Total-Count'; set $cors_credentials 'true'; } @@ -108,6 +110,7 @@ location @mempool-api-v1-services-cache-disabled-addcors { add_header Access-Control-Allow-Origin "$cors_origin" always; add_header Access-Control-Allow-Headers "$cors_headers" always; add_header Access-Control-Allow-Credentials "$cors_credentials" always; + add_header Access-Control-Expose-Headers "$cors_expose_headers" always; proxy_redirect off; proxy_buffering off; @@ -172,6 +175,7 @@ location @mempool-api-v1-services-cache-short-addcors { set $cors_methods 'GET, POST, PUT, DELETE, OPTIONS'; set $cors_origin 'https://mempool.space'; set $cors_headers 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With'; + set $cors_expose_headers 'X-Total-Count'; set $cors_credentials 'true'; # set CORS for approved hostnames @@ -180,6 +184,7 @@ location @mempool-api-v1-services-cache-short-addcors { set $cors_methods 'GET, POST, PUT, DELETE, OPTIONS'; set $cors_origin "$http_origin"; set $cors_headers 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With'; + set $cors_expose_headers 'X-Total-Count'; set $cors_credentials 'true'; } @@ -188,6 +193,7 @@ location @mempool-api-v1-services-cache-short-addcors { add_header Access-Control-Allow-Origin "$cors_origin" always; add_header Access-Control-Allow-Headers "$cors_headers" always; add_header Access-Control-Allow-Credentials "$cors_credentials" always; + add_header Access-Control-Expose-Headers "$cors_expose_headers" always; # add our own cache headers add_header 'Pragma' 'public'; diff --git a/production/nginx/server-common.conf b/production/nginx/server-common.conf index 2f84cda7f..9a2a582c0 100644 --- a/production/nginx/server-common.conf +++ b/production/nginx/server-common.conf @@ -8,8 +8,11 @@ add_header Onion-Location http://$onion.onion$request_uri; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; # generate frame configuration from origin header -set $frameOptions "DENY"; -set $contentSecurityPolicy "frame-ancestors 'none'"; +if ($frameOptions = '') +{ + set $frameOptions "DENY"; + set $contentSecurityPolicy "frame-ancestors 'none'"; +} # used for iframes on https://mempool.space/network if ($http_referer ~ ^https://mempool.space/)