diff --git a/LICENSE b/LICENSE index b6a09390a..1c368c00a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ The Mempool Open Source Project® -Copyright (c) 2019-2023 Mempool Space K.K. and other shadowy super-coders +Copyright (c) 2019-2024 Mempool Space K.K. and other shadowy super-coders This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free @@ -12,10 +12,12 @@ or any other contributor to The Mempool Open Source Project. The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full -Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square logo, -the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical -Logo, and the mempool.space Horizontal logo are registered trademarks or trademarks -of Mempool Space K.K in Japan, the United States, and/or other countries. +Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo, +the mempool block visualization Logo, the mempool Blocks Logo, the mempool +transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, +the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are +registered trademarks or trademarks of Mempool Space K.K in Japan, +the United States, and/or other countries. See our full Trademark Policy and Guidelines for more details, published on . diff --git a/backend/README.md b/backend/README.md index 6ae4ae3e2..cecc07bc9 100644 --- a/backend/README.md +++ b/backend/README.md @@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec) #### Build -_Make sure to use Node.js 16.10 and npm 7._ +_Make sure to use Node.js 20.x and npm 9.x or newer_ _The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._ 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 04e15afa2..e0d28bfc9 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,25 +1,25 @@ { "name": "mempool-backend", - "version": "3.0.0-dev", + "version": "3.1.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mempool-backend", - "version": "3.0.0-dev", + "version": "3.1.0-dev", "hasInstallScript": true, "license": "GNU Affero General Public License v3.0", "dependencies": { - "@babel/core": "^7.24.0", + "@babel/core": "^7.25.2", "@mempool/electrum-client": "1.1.9", "@types/node": "^18.15.3", - "axios": "~1.7.2", + "axios": "1.7.2", "bitcoinjs-lib": "~6.1.3", "crypto-js": "~4.2.0", - "express": "~4.19.2", + "express": "~4.21.1", "maxmind": "~4.3.11", - "mysql2": "~3.10.0", - "redis": "^4.6.6", + "mysql2": "~3.11.0", + "redis": "^4.7.0", "rust-gbt": "file:./rust-gbt", "socks-proxy-agent": "~7.0.0", "typescript": "~4.9.3", @@ -27,7 +27,7 @@ }, "devDependencies": { "@babel/code-frame": "^7.18.6", - "@babel/core": "^7.24.0", + "@babel/core": "^7.25.2", "@types/compression": "^1.7.2", "@types/crypto-js": "^4.1.1", "@types/express": "^4.17.17", @@ -73,43 +73,43 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", + "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", - "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.0", - "@babel/parser": "^7.24.0", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.0", - "@babel/types": "^7.24.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -131,14 +131,14 @@ "dev": true }, "node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", "dev": true, "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.25.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { @@ -146,28 +146,28 @@ } }, "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -175,63 +175,29 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.15" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" }, "engines": { "node": ">=6.9.0" @@ -250,88 +216,77 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz", - "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", + "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", "dev": true, "dependencies": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.0", - "@babel/types": "^7.24.0" + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", - "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.0.tgz", + "integrity": "sha512-CzdIU9jdP0dg7HdyB+bHvDJGagUv+qtzZt5rYCWwW6tITNqV9odjp6Qu41gkG0ca5UfdDUWrKkiAnHHdGRnOrA==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -518,33 +473,30 @@ } }, "node_modules/@babel/template": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz", - "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.2.tgz", + "integrity": "sha512-s4/r+a7xTnny2O6FcZzqgT6nE4/GHEdcqj4qAeglbUOh0TeglEfmNJFAd/OLoVtGd6ZhAO8GCVvCNUO5t/VJVQ==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.2", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -553,13 +505,13 @@ } }, "node_modules/@babel/types": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1475,9 +1427,9 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" @@ -1490,13 +1442,13 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@mempool/electrum-client": { @@ -1562,9 +1514,9 @@ } }, "node_modules/@redis/client": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.7.tgz", - "integrity": "sha512-gaOBOuJPjK5fGtxSseaKgSvjiZXQCdLlGg9WYQst+/GRUjmXaiB5kVkeQMRtPc7Q2t93XZcJfBMSwzs/XS9UZw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", + "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -1580,33 +1532,33 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/@redis/graph": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz", - "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", "peerDependencies": { "@redis/client": "^1.0.0" } }, "node_modules/@redis/json": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz", - "integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", "peerDependencies": { "@redis/client": "^1.0.0" } }, "node_modules/@redis/search": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.2.tgz", - "integrity": "sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", "peerDependencies": { "@redis/client": "^1.0.0" } }, "node_modules/@redis/time-series": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz", - "integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", "peerDependencies": { "@redis/client": "^1.0.0" } @@ -2317,10 +2269,19 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.1.tgz", + "integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ==", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/axios": { "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", @@ -2529,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", @@ -2541,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" @@ -2587,9 +2548,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", + "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", "dev": true, "funding": [ { @@ -2606,10 +2567,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", + "caniuse-lite": "^1.0.30001640", + "electron-to-chromium": "^1.4.820", "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -2707,9 +2668,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001591", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz", - "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==", + "version": "1.0.30001644", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001644.tgz", + "integrity": "sha512-YGvlOZB4QhZuiis+ETS0VXR+MExbFf4fZYYeMTEE0aTQd/RdIjkTyZjLrbYVKnHzppDvnOhritRVv+i7Go6mHw==", "dev": true, "funding": [ { @@ -2866,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" } @@ -3046,9 +3007,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.686", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.686.tgz", - "integrity": "sha512-3avY1B+vUzNxEgkBDpKOP8WarvUAEwpRaiCL0He5OKWEFxzaOFiq4WoZEZe7qh0ReS7DiWoHMnYoQCKxNZNzSg==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz", + "integrity": "sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==", "dev": true }, "node_modules/emittery": { @@ -3070,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" } @@ -3106,9 +3067,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, "engines": { "node": ">=6" @@ -3500,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", @@ -3642,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", @@ -6091,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", @@ -6197,10 +6161,11 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mysql2": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.10.0.tgz", - "integrity": "sha512-qx0mfWYt1DpTPkw8mAcHW/OwqqyNqBLBHvY5IjN8+icIYTjt6znrgYJ+gxqNNRpVknb5Wc/gcCM4XjbCR0j5tw==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz", + "integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==", "dependencies": { + "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.6.3", @@ -6279,9 +6244,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, "node_modules/normalize-path": { @@ -6306,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" } @@ -6476,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", @@ -6490,9 +6458,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "dev": true }, "node_modules/picomatch": { @@ -6686,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" @@ -6748,16 +6716,16 @@ "dev": true }, "node_modules/redis": { - "version": "4.6.6", - "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.6.tgz", - "integrity": "sha512-aLs2fuBFV/VJ28oLBqYykfnhGGkFxvx0HdCEBYdJ99FFbSEMZ7c1nVKwR6ZRv+7bb7JnC0mmCzaqu8frgOYhpA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz", + "integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==", "dependencies": { "@redis/bloom": "1.2.0", - "@redis/client": "1.5.7", - "@redis/graph": "1.1.0", - "@redis/json": "1.0.4", - "@redis/search": "1.1.2", - "@redis/time-series": "1.0.4" + "@redis/client": "1.6.0", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" } }, "node_modules/require-directory": { @@ -6911,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", @@ -6946,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", @@ -6957,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" @@ -7514,9 +7490,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "dev": true, "funding": [ { @@ -7533,8 +7509,8 @@ } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -7794,37 +7770,37 @@ } }, "@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, "requires": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" } }, "@babel/compat-data": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", + "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", "dev": true }, "@babel/core": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", - "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", "dev": true, "requires": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.0", - "@babel/parser": "^7.24.0", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.0", - "@babel/types": "^7.24.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -7841,88 +7817,63 @@ } }, "@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", "dev": true, "requires": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.25.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "dependencies": { "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "requires": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" } } } }, "@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", "dev": true, "requires": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, - "@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true - }, - "@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "requires": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, "@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dev": true, "requires": { - "@babel/types": "^7.22.15" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" } }, "@babel/helper-plugin-utils": { @@ -7932,67 +7883,59 @@ "dev": true }, "@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dev": true, "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true }, "@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", "dev": true }, "@babel/helpers": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz", - "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", + "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", "dev": true, "requires": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.0", - "@babel/types": "^7.24.0" + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.0" } }, "@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" } }, "@babel/parser": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", - "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.0.tgz", + "integrity": "sha512-CzdIU9jdP0dg7HdyB+bHvDJGagUv+qtzZt5rYCWwW6tITNqV9odjp6Qu41gkG0ca5UfdDUWrKkiAnHHdGRnOrA==", "dev": true }, "@babel/plugin-syntax-async-generators": { @@ -8122,42 +8065,39 @@ } }, "@babel/template": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", "dev": true, "requires": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" } }, "@babel/traverse": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz", - "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.2.tgz", + "integrity": "sha512-s4/r+a7xTnny2O6FcZzqgT6nE4/GHEdcqj4qAeglbUOh0TeglEfmNJFAd/OLoVtGd6ZhAO8GCVvCNUO5t/VJVQ==", "dev": true, "requires": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.2", "debug": "^4.3.1", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" } }, @@ -8847,9 +8787,9 @@ "dev": true }, "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true }, "@jridgewell/sourcemap-codec": { @@ -8859,13 +8799,13 @@ "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "@mempool/electrum-client": { @@ -8911,9 +8851,9 @@ "requires": {} }, "@redis/client": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.7.tgz", - "integrity": "sha512-gaOBOuJPjK5fGtxSseaKgSvjiZXQCdLlGg9WYQst+/GRUjmXaiB5kVkeQMRtPc7Q2t93XZcJfBMSwzs/XS9UZw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", + "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", "requires": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -8928,27 +8868,27 @@ } }, "@redis/graph": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz", - "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", "requires": {} }, "@redis/json": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz", - "integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", "requires": {} }, "@redis/search": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.2.tgz", - "integrity": "sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", "requires": {} }, "@redis/time-series": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz", - "integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", "requires": {} }, "@sinclair/typebox": { @@ -9508,6 +9448,11 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "aws-ssl-profiles": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.1.tgz", + "integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ==" + }, "axios": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", @@ -9674,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", @@ -9686,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" @@ -9727,15 +9672,15 @@ } }, "browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", + "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", + "caniuse-lite": "^1.0.30001640", + "electron-to-chromium": "^1.4.820", "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "update-browserslist-db": "^1.1.0" } }, "bs-logger": { @@ -9809,9 +9754,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001591", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz", - "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==", + "version": "1.0.30001644", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001644.tgz", + "integrity": "sha512-YGvlOZB4QhZuiis+ETS0VXR+MExbFf4fZYYeMTEE0aTQd/RdIjkTyZjLrbYVKnHzppDvnOhritRVv+i7Go6mHw==", "dev": true }, "chalk": { @@ -9920,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", @@ -10049,9 +9994,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "electron-to-chromium": { - "version": "1.4.686", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.686.tgz", - "integrity": "sha512-3avY1B+vUzNxEgkBDpKOP8WarvUAEwpRaiCL0He5OKWEFxzaOFiq4WoZEZe7qh0ReS7DiWoHMnYoQCKxNZNzSg==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz", + "integrity": "sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==", "dev": true }, "emittery": { @@ -10067,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", @@ -10094,9 +10039,9 @@ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true }, "escape-html": { @@ -10374,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", @@ -10505,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", @@ -12307,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", @@ -12382,10 +12327,11 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "mysql2": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.10.0.tgz", - "integrity": "sha512-qx0mfWYt1DpTPkw8mAcHW/OwqqyNqBLBHvY5IjN8+icIYTjt6znrgYJ+gxqNNRpVknb5Wc/gcCM4XjbCR0j5tw==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz", + "integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==", "requires": { + "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.6.3", @@ -12450,9 +12396,9 @@ "dev": true }, "node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, "normalize-path": { @@ -12471,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", @@ -12590,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", @@ -12601,9 +12547,9 @@ "dev": true }, "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "dev": true }, "picomatch": { @@ -12734,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": { @@ -12770,16 +12716,16 @@ "dev": true }, "redis": { - "version": "4.6.6", - "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.6.tgz", - "integrity": "sha512-aLs2fuBFV/VJ28oLBqYykfnhGGkFxvx0HdCEBYdJ99FFbSEMZ7c1nVKwR6ZRv+7bb7JnC0mmCzaqu8frgOYhpA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz", + "integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==", "requires": { "@redis/bloom": "1.2.0", - "@redis/client": "1.5.7", - "@redis/graph": "1.1.0", - "@redis/json": "1.0.4", - "@redis/search": "1.1.2", - "@redis/time-series": "1.0.4" + "@redis/client": "1.6.0", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" } }, "require-directory": { @@ -12872,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", @@ -12906,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", @@ -12919,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": { @@ -13299,13 +13250,13 @@ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, "update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "dev": true, "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" } }, "uri-js": { diff --git a/backend/package.json b/backend/package.json index 24e76e3dd..9ac3f9199 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "mempool-backend", - "version": "3.0.0-dev", + "version": "3.1.0-dev", "description": "Bitcoin mempool visualizer and blockchain explorer backend", "license": "GNU Affero General Public License v3.0", "homepage": "https://mempool.space", @@ -39,24 +39,24 @@ "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"" }, "dependencies": { - "@babel/core": "^7.24.0", + "@babel/core": "^7.25.2", "@mempool/electrum-client": "1.1.9", "@types/node": "^18.15.3", - "axios": "~1.7.2", + "axios": "1.7.2", "bitcoinjs-lib": "~6.1.3", "crypto-js": "~4.2.0", - "express": "~4.19.2", + "express": "~4.21.1", "maxmind": "~4.3.11", - "mysql2": "~3.10.0", + "mysql2": "~3.11.0", "rust-gbt": "file:./rust-gbt", - "redis": "^4.6.6", + "redis": "^4.7.0", "socks-proxy-agent": "~7.0.0", "typescript": "~4.9.3", "ws": "~8.18.0" }, "devDependencies": { "@babel/code-frame": "^7.18.6", - "@babel/core": "^7.24.0", + "@babel/core": "^7.25.2", "@types/compression": "^1.7.2", "@types/crypto-js": "^4.1.1", "@types/express": "^4.17.17", 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/about.routes.ts b/backend/src/api/about.routes.ts index 5e7d3b70e..2020d111d 100644 --- a/backend/src/api/about.routes.ts +++ b/backend/src/api/about.routes.ts @@ -70,7 +70,7 @@ class AboutRoutes { res.status(500).end(); } }) - .get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username', async (req, res) => { + .get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username/:md5', async (req, res) => { const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`; try { const response = await axios.get(url, { responseType: 'stream', timeout: 10000 }); diff --git a/backend/src/api/audit.ts b/backend/src/api/audit.ts index 4d05870e8..e09234cdc 100644 --- a/backend/src/api/audit.ts +++ b/backend/src/api/audit.ts @@ -2,24 +2,28 @@ import config from '../config'; import logger from '../logger'; import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces'; import rbfCache from './rbf-cache'; +import transactionUtils from './transaction-utils'; const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners class Audit { - auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false) - : { censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } { + auditBlock(height: number, transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }) + : { unseen: string[], censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } { if (!projectedBlocks?.[0]?.transactionIds || !mempool) { - return { censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 }; + return { unseen: [], censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 }; } const matches: string[] = []; // present in both mined block and template const added: string[] = []; // present in mined block, not in template - const prioritized: string[] = [] // present in the mined block, not in the template, but further down in the mempool + const unseen: string[] = []; // present in the mined block, not in our mempool + let prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone + let deprioritized: string[] = []; // lower in the block than would be expected by in-band feerate alone const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block const accelerated: string[] = []; // prioritized by the mempool accelerator const isCensored = {}; // missing, without excuse const isDisplaced = {}; + const isAccelerated = {}; let displacedWeight = 0; let matchedWeight = 0; let projectedWeight = 0; @@ -32,6 +36,7 @@ class Audit { inBlock[tx.txid] = tx; if (mempool[tx.txid] && mempool[tx.txid].acceleration) { accelerated.push(tx.txid); + isAccelerated[tx.txid] = true; } } // coinbase is always expected @@ -113,11 +118,16 @@ class Audit { } else { if (rbfCache.has(tx.txid)) { rbf.push(tx.txid); - } else if (!isDisplaced[tx.txid]) { + if (!mempool[tx.txid] && !rbfCache.getReplacedBy(tx.txid)) { + unseen.push(tx.txid); + } + } else { if (mempool[tx.txid]) { - prioritized.push(tx.txid); + if (isDisplaced[tx.txid]) { + added.push(tx.txid); + } } else { - added.push(tx.txid); + unseen.push(tx.txid); } } overflowWeight += tx.weight; @@ -125,6 +135,8 @@ class Audit { totalWeight += tx.weight; } + ({ prioritized, deprioritized } = transactionUtils.identifyPrioritizedTransactions(transactions, 'effectiveFeePerVsize')); + // transactions missing from near the end of our template are probably not being censored let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight); let maxOverflowRate = 0; @@ -165,6 +177,7 @@ class Audit { const similarity = projectedWeight ? matchedWeight / projectedWeight : 1; return { + unseen, censored: Object.keys(isCensored), added, prioritized, diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index 8413afd9f..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,6 +23,7 @@ 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; 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 0b62f672d..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 { @@ -327,6 +331,7 @@ class BitcoinApi implements AbstractBitcoinApi { 'witness_v1_taproot': 'v1_p2tr', 'nonstandard': 'nonstandard', 'multisig': 'multisig', + 'anchor': 'anchor', 'nulldata': 'op_return' }; 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 a65af3f19..d2d298e09 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) { @@ -41,12 +42,15 @@ class BitcoinRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions) + .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/summary', this.getStrippedBlockTransaction) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight) .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 +90,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 +109,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 +132,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 +145,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; } @@ -165,6 +169,7 @@ class BitcoinRoutes { acceleration: tx.acceleration, acceleratedBy: tx.acceleratedBy || undefined, acceleratedAt: tx.acceleratedAt || undefined, + feeDelta: tx.feeDelta || undefined, }); return; } @@ -179,7 +184,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; } } @@ -208,7 +213,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); } } @@ -222,7 +227,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); } } @@ -283,13 +288,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); } } } @@ -303,7 +308,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); } } @@ -312,6 +317,20 @@ class BitcoinRoutes { const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.json(transactions); + } catch (e) { + handleError(req, res, 500, e instanceof Error ? e.message : e); + } + } + + private async getStrippedBlockTransaction(req: Request, res: Response) { + try { + const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid); + if (!transaction) { + handleError(req, res, 404, `transaction not found in summary`); + return; + } + res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); + res.json(transaction); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); } @@ -335,7 +354,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); } } @@ -345,7 +364,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); } } @@ -356,10 +375,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); } } @@ -370,7 +390,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); @@ -387,42 +408,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); } } @@ -457,10 +485,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); @@ -482,7 +510,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); } } @@ -491,13 +519,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; } @@ -506,15 +534,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; } @@ -527,23 +556,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; } @@ -554,15 +583,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; } @@ -577,16 +607,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; } } @@ -596,7 +626,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); } } @@ -623,7 +653,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); } } @@ -631,12 +661,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); } } @@ -646,7 +677,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); } } @@ -656,7 +687,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); } } @@ -665,7 +696,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); } } @@ -674,7 +705,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); } } @@ -687,7 +718,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); } } @@ -696,7 +727,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); } } @@ -705,7 +736,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); } } @@ -718,7 +749,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); } } @@ -727,7 +758,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); } } @@ -737,10 +768,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); } } @@ -751,7 +782,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')); } } @@ -763,7 +794,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')); } } @@ -775,8 +806,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.ts b/backend/src/api/bitcoin/esplora-api.ts index a27ca35e9..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); } diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 762c81ff7..e621056ab 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -33,6 +33,8 @@ import AccelerationRepository from '../repositories/AccelerationRepository'; 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[] = []; @@ -218,10 +220,10 @@ class Blocks { }; } - public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary { + public summarizeBlockTransactions(hash: string, height: number, transactions: TransactionExtended[]): BlockSummary { return { id: hash, - transactions: Common.classifyTransactions(transactions), + transactions: Common.classifyTransactions(transactions, height), }; } @@ -341,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; @@ -405,8 +412,16 @@ class Blocks { } try { + const blockchainInfo = await bitcoinClient.getBlockchainInfo(); + const currentBlockHeight = blockchainInfo.blocks; + let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, currentBlockHeight); + if (indexingBlockAmount <= -1) { + indexingBlockAmount = currentBlockHeight + 1; + } + const lastBlockToIndex = Math.max(0, currentBlockHeight - indexingBlockAmount + 1); + // Get all indexed block hash - const indexedBlocks = await blocksRepository.$getIndexedBlocks(); + const indexedBlocks = (await blocksRepository.$getIndexedBlocks()).filter(block => block.height >= lastBlockToIndex); const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId(); const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop @@ -439,7 +454,7 @@ class Blocks { if (config.MEMPOOL.BACKEND === 'esplora') { - const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx)); + const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendMempoolTransaction(tx)); const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs); if (cpfpSummary) { await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary @@ -615,7 +630,7 @@ class Blocks { // add CPFP const cpfpSummary = calculateGoodBlockCpfp(height, txs, []); // classify - const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); + const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions); await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2); if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) { const cpfpClusters = await CpfpRepository.$getClustersAt(height); @@ -652,7 +667,7 @@ class Blocks { } const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []); // classify - const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); + const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions); const classifiedTxMap: { [txid: string]: TransactionClassified } = {}; for (const tx of classifiedTxs) { classifiedTxMap[tx.txid] = tx; @@ -904,9 +919,14 @@ class Blocks { } } - const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, Object.values(mempool.getAccelerations()).map(a => ({ txid: a.txid, max_bid: a.feeDelta }))); + let accelerations = Object.values(mempool.getAccelerations()); + if (accelerations?.length > 0) { + const pool = await this.$findBlockMiner(transactionUtils.stripCoinbaseTransaction(transactions[0])); + accelerations = accelerations.filter(a => a.pools.includes(pool.uniqueId)); + } + const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta }))); const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions); - const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions); + const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, block.height, cpfpSummary.transactions); this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`); if (Common.indexingEnabled()) { @@ -927,12 +947,12 @@ class Blocks { const newBlock = await this.$indexBlock(lastBlock.height - i); this.blocks.push(newBlock); this.updateTimerProgress(timer, `reindexed block`); - let cpfpSummary; + let newCpfpSummary; if (config.MEMPOOL.CPFP_INDEXING) { - cpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i); + newCpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i); this.updateTimerProgress(timer, `reindexed block cpfp`); } - await this.$getStrippedBlockTransactions(newBlock.id, true, true, cpfpSummary, newBlock.height); + await this.$getStrippedBlockTransactions(newBlock.id, true, true, newCpfpSummary, newBlock.height); this.updateTimerProgress(timer, `reindexed block summary`); } await mining.$indexDifficultyAdjustments(); @@ -981,7 +1001,7 @@ class Blocks { // start async callbacks this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`); - const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions)); + const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, cpfpSummary.transactions)); if (block.height % 2016 === 0) { if (Common.indexingEnabled()) { @@ -1163,7 +1183,7 @@ class Blocks { transactions: cpfpSummary.transactions.map(tx => { let flags: number = 0; try { - flags = Common.getTransactionFlags(tx); + flags = Common.getTransactionFlags(tx, height); } catch (e) { logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e)); } @@ -1178,11 +1198,11 @@ class Blocks { }; }), }; - summaryVersion = 1; + summaryVersion = cpfpSummary.version; } else { if (config.MEMPOOL.BACKEND === 'esplora') { const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx)); - summary = this.summarizeBlockTransactions(hash, txs); + summary = this.summarizeBlockTransactions(hash, height || 0, txs); summaryVersion = 1; } else { // Call Core RPC @@ -1204,6 +1224,11 @@ class Blocks { return summary.transactions; } + public async $getSingleTxFromSummary(hash: string, txid: string): Promise { + const txs = await this.$getStrippedBlockTransactions(hash); + return txs.find(tx => tx.txid === txid) || null; + } + /** * Get 15 blocks * @@ -1318,7 +1343,7 @@ class Blocks { let summaryVersion = 0; if (config.MEMPOOL.BACKEND === 'esplora') { const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx)); - summary = this.summarizeBlockTransactions(cleanBlock.hash, txs); + summary = this.summarizeBlockTransactions(cleanBlock.hash, cleanBlock.height, txs); summaryVersion = 1; } else { // Call Core RPC @@ -1397,11 +1422,11 @@ class Blocks { return this.currentBlockHeight; } - public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise { + public async $indexCPFP(hash: string, height: number, txs?: MempoolTransactionExtended[]): Promise { let transactions = txs; if (!transactions) { if (config.MEMPOOL.BACKEND === 'esplora') { - transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx)); + transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendMempoolTransaction(tx)); } if (!transactions) { const block = await bitcoinClient.getBlock(hash, 2); @@ -1413,7 +1438,7 @@ class Blocks { } if (transactions?.length != null) { - const summary = calculateFastBlockCpfp(height, transactions as TransactionExtended[]); + const summary = calculateFastBlockCpfp(height, transactions); await this.$saveCpfp(hash, height, summary); diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index cba39a511..50de63afc 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -1,6 +1,6 @@ import * as bitcoinjs from 'bitcoinjs-lib'; import { Request } from 'express'; -import { CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces'; +import { EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces'; import config from '../config'; import { NodeSocket } from '../repositories/NodesSocketsRepository'; import { isIP } from 'net'; @@ -10,7 +10,6 @@ import logger from '../logger'; import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script'; // Bitcoin Core default policy settings -const TX_MAX_STANDARD_VERSION = 2; const MAX_STANDARD_TX_WEIGHT = 400_000; const MAX_BLOCK_SIGOPS_COST = 80_000; const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5); @@ -80,8 +79,8 @@ export class Common { return arr; } - static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } { - const matches: { [txid: string]: MempoolTransactionExtended[] } = {}; + static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} { + const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {}; // For small N, a naive nested loop is extremely fast, but it doesn't scale if (added.length < 1000 && deleted.length < 50 && !forceScalable) { @@ -96,7 +95,7 @@ export class Common { addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout)); }); if (foundMatches?.length) { - matches[addedTx.txid] = [...new Set(foundMatches)]; + matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx }; } }); } else { @@ -124,7 +123,7 @@ export class Common { foundMatches.add(deletedTx); } if (foundMatches.size) { - matches[addedTx.txid] = [...foundMatches]; + matches[addedTx.txid] = { replaced: [...foundMatches], replacedBy: addedTx }; } } } @@ -139,17 +138,17 @@ export class Common { const replaced: Set = new Set(); for (let i = 0; i < tx.vin.length; i++) { const vin = tx.vin[i]; - const match = spendMap.get(`${vin.txid}:${vin.vout}`); + const key = `${vin.txid}:${vin.vout}`; + const match = spendMap.get(key); if (match && match.txid !== tx.txid) { replaced.add(match); // remove this tx from the spendMap // prevents the same tx being replaced more than once for (const replacedVin of match.vin) { - const key = `${replacedVin.txid}:${replacedVin.vout}`; - spendMap.delete(key); + const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`; + spendMap.delete(replacedKey); } } - const key = `${vin.txid}:${vin.vout}`; spendMap.delete(key); } if (replaced.size) { @@ -200,10 +199,13 @@ export class Common { * * returns true early if any standardness rule is violated, otherwise false * (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced) + * + * As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks. + * For now, just pull out individual rules into versioned functions where necessary. */ - static isNonStandard(tx: TransactionExtended): boolean { + static isNonStandard(tx: TransactionExtended, height?: number): boolean { // version - if (tx.version > TX_MAX_STANDARD_VERSION) { + if (this.isNonStandardVersion(tx, height)) { return true; } @@ -250,6 +252,8 @@ export class Common { } } else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) { return true; + } else if (this.isNonStandardAnchor(tx, height)) { + return true; } // TODO: bad-witness-nonstandard } @@ -335,6 +339,49 @@ export class Common { return false; } + // Individual versioned standardness rules + + static V3_STANDARDNESS_ACTIVATION_HEIGHT = { + 'testnet4': 42_000, + 'testnet': 2_900_000, + 'signet': 211_000, + '': 863_500, + }; + static isNonStandardVersion(tx: TransactionExtended, height?: number): boolean { + let TX_MAX_STANDARD_VERSION = 3; + if ( + height != null + && this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK] + && height <= this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK] + ) { + // V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891) + TX_MAX_STANDARD_VERSION = 2; + } + + if (tx.version > TX_MAX_STANDARD_VERSION) { + return true; + } + return false; + } + + static ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = { + 'testnet4': 42_000, + 'testnet': 2_900_000, + 'signet': 211_000, + '': 863_500, + }; + static isNonStandardAnchor(tx: TransactionExtended, height?: number): boolean { + if ( + height != null + && this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK] + && height <= this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK] + ) { + // anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891) + return true; + } + return false; + } + static getNonWitnessSize(tx: TransactionExtended): number { let weight = tx.weight; let hasWitness = false; @@ -415,7 +462,7 @@ export class Common { return flags; } - static getTransactionFlags(tx: TransactionExtended): number { + static getTransactionFlags(tx: TransactionExtended, height?: number): number { let flags = tx.flags ? BigInt(tx.flags) : 0n; // Update variable flags (CPFP, RBF) @@ -548,7 +595,7 @@ export class Common { if (hasFakePubkey) { flags |= TransactionFlags.fake_pubkey; } - + // fast but bad heuristic to detect possible coinjoins // (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse) const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1; @@ -564,17 +611,17 @@ export class Common { flags |= TransactionFlags.batch_payout; } - if (this.isNonStandard(tx)) { + if (this.isNonStandard(tx, height)) { flags |= TransactionFlags.nonstandard; } return Number(flags); } - static classifyTransaction(tx: TransactionExtended): TransactionClassified { + static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified { let flags = 0; try { - flags = Common.getTransactionFlags(tx); + flags = Common.getTransactionFlags(tx, height); } catch (e) { logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e)); } @@ -585,8 +632,8 @@ export class Common { }; } - static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] { - return txs.map(Common.classifyTransaction); + static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] { + return txs.map(tx => Common.classifyTransaction(tx, height)); } static stripTransaction(tx: TransactionExtended): TransactionStripped { diff --git a/backend/src/api/cpfp.ts b/backend/src/api/cpfp.ts index 5818eb1ea..9da11328b 100644 --- a/backend/src/api/cpfp.ts +++ b/backend/src/api/cpfp.ts @@ -6,7 +6,7 @@ import { Acceleration } from './acceleration/acceleration'; const CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction const MAX_CLUSTER_ITERATIONS = 100; -export function calculateFastBlockCpfp(height: number, transactions: TransactionExtended[], saveRelatives: boolean = false): CpfpSummary { +export function calculateFastBlockCpfp(height: number, transactions: MempoolTransactionExtended[], saveRelatives: boolean = false): CpfpSummary { const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster @@ -93,6 +93,7 @@ export function calculateFastBlockCpfp(height: number, transactions: Transaction return { transactions, clusters, + version: 1, }; } @@ -159,6 +160,7 @@ export function calculateGoodBlockCpfp(height: number, transactions: MempoolTran return { transactions: transactions.map(tx => txMap[tx.txid]), clusters: clusterArray, + version: 2, }; } diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 70ff2d5bb..6cb361ffd 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 = 80; + private static currentVersion = 93; private queryTimeout = 3600_000; private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; @@ -653,9 +653,11 @@ class DatabaseMigration { await this.$executeQuery('ALTER TABLE `prices` ADD `TRY` float DEFAULT "-1"'); await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"'); - await this.$executeQuery('TRUNCATE hashrates'); - await this.$executeQuery('TRUNCATE difficulty_adjustments'); - await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`); + if (isBitcoin === true) { + await this.$executeQuery('TRUNCATE hashrates'); + await this.$executeQuery('TRUNCATE difficulty_adjustments'); + await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`); + } await this.updateToSchemaVersion(75); } @@ -691,6 +693,114 @@ class DatabaseMigration { await this.$executeQuery('ALTER TABLE `blocks` ADD coinbase_addresses JSON DEFAULT NULL'); await this.updateToSchemaVersion(80); } + + if (databaseSchemaVersion < 81 && isBitcoin === true) { + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD version INT NOT NULL DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `version` (`version`)'); + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"'); + await this.updateToSchemaVersion(81); + } + + if (databaseSchemaVersion < 82 && isBitcoin === true && config.MEMPOOL.NETWORK === 'mainnet') { + await this.$fixBadV1AuditBlocks(); + await this.updateToSchemaVersion(82); + } + + if (databaseSchemaVersion < 83 && isBitcoin === true) { + await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL'); + await this.updateToSchemaVersion(83); + } + + // add new pools indexes + if (databaseSchemaVersion < 84 && isBitcoin === true) { + await this.$executeQuery(` + ALTER TABLE \`pools\` + ADD INDEX \`slug\` (\`slug\`), + ADD INDEX \`unique_id\` (\`unique_id\`) + `); + await this.updateToSchemaVersion(84); + } + + // lightning channels indexes + if (databaseSchemaVersion < 85 && isBitcoin === true) { + await this.$executeQuery(` + ALTER TABLE \`channels\` + ADD INDEX \`created\` (\`created\`), + ADD INDEX \`capacity\` (\`capacity\`), + ADD INDEX \`closing_reason\` (\`closing_reason\`), + ADD INDEX \`closing_resolved\` (\`closing_resolved\`) + `); + await this.updateToSchemaVersion(85); + } + + // lightning nodes indexes + if (databaseSchemaVersion < 86 && isBitcoin === true) { + await this.$executeQuery(` + ALTER TABLE \`nodes\` + ADD INDEX \`status\` (\`status\`), + ADD INDEX \`channels\` (\`channels\`), + ADD INDEX \`country_id\` (\`country_id\`), + ADD INDEX \`as_number\` (\`as_number\`), + ADD INDEX \`first_seen\` (\`first_seen\`) + `); + await this.updateToSchemaVersion(86); + } + + // lightning node sockets indexes + if (databaseSchemaVersion < 87 && isBitcoin === true) { + await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)'); + await this.updateToSchemaVersion(87); + } + + // lightning stats indexes + if (databaseSchemaVersion < 88 && isBitcoin === true) { + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)'); + await this.updateToSchemaVersion(88); + } + + // geo names indexes + if (databaseSchemaVersion < 89 && isBitcoin === true) { + await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)'); + await this.updateToSchemaVersion(89); + } + + // hashrates indexes + if (databaseSchemaVersion < 90 && isBitcoin === true) { + await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)'); + await this.updateToSchemaVersion(90); + } + + // block audits indexes + if (databaseSchemaVersion < 91 && isBitcoin === true) { + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)'); + await this.updateToSchemaVersion(91); + } + + // elements_pegs indexes + if (databaseSchemaVersion < 92 && config.MEMPOOL.NETWORK === 'liquid') { + await this.$executeQuery(` + ALTER TABLE \`elements_pegs\` + ADD INDEX \`block\` (\`block\`), + ADD INDEX \`datetime\` (\`datetime\`), + ADD INDEX \`amount\` (\`amount\`), + ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`), + ADD INDEX \`bitcointxid\` (\`bitcointxid\`) + `); + await this.updateToSchemaVersion(92); + } + + // federation_txos indexes + if (databaseSchemaVersion < 93 && config.MEMPOOL.NETWORK === 'liquid') { + await this.$executeQuery(` + ALTER TABLE \`federation_txos\` + ADD INDEX \`unspent\` (\`unspent\`), + ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`), + ADD INDEX \`blocktime\` (\`blocktime\`), + ADD INDEX \`emergencyKey\` (\`emergencyKey\`), + ADD INDEX \`expiredAt\` (\`expiredAt\`) + `); + await this.updateToSchemaVersion(93); + } } /** @@ -1305,6 +1415,28 @@ class DatabaseMigration { logger.warn(`Failed to migrate cpfp transaction data`); } } + + private async $fixBadV1AuditBlocks(): Promise { + const badBlocks = [ + '000000000000000000011ad49227fc8c9ba0ca96ad2ebce41a862f9a244478dc', + '000000000000000000010ac1f68b3080153f2826ffddc87ceffdd68ed97d6960', + '000000000000000000024cbdafeb2660ae8bd2947d166e7fe15d1689e86b2cf7', + '00000000000000000002e1dbfbf6ae057f331992a058b822644b368034f87286', + '0000000000000000000019973b2778f08ad6d21e083302ff0833d17066921ebb', + ]; + + for (const hash of badBlocks) { + try { + await this.$executeQuery(` + UPDATE blocks_audits + SET prioritized_txs = '[]' + WHERE hash = '${hash}' + `, true); + } catch (e) { + continue; + } + } + } } export default new DatabaseMigration(); 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 e655601e5..ba4ce2ed0 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,8 +460,9 @@ class MempoolBlocks { } } - for (const txid of block) { - if (txid) { + for (let i = 0; i < block.length; i++) { + const txid = block[i]; + if (txid in mempool) { mempoolTx = mempool[txid]; // save position in projected blocks mempoolTx.position = { @@ -445,28 +470,40 @@ 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; - 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 (!(ancestor.txid in mempool)) { + continue; + } + 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; - 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 @@ -484,7 +521,7 @@ class MempoolBlocks { } } } - return this.dataToMempoolBlocks( + mempoolBlocks[blockIndex] = this.dataToMempoolBlocks( block, transactions, totalSize, @@ -492,7 +529,7 @@ class MempoolBlocks { totalFees, (hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined, ); - }); + }; if (saveResults) { const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks); @@ -654,7 +691,7 @@ class MempoolBlocks { [pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean }; } = {}; // prepare a list of accelerations in ascending order (we'll pop items off the end of the list) - const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).map(acc => { + const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).filter(acc => acc.txid in mempoolCache).map(acc => { let vsize = mempoolCache[acc.txid].vsize; for (const ancestor of mempoolCache[acc.txid].ancestors || []) { vsize += (ancestor.weight / 4); 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/mini-miner.ts b/backend/src/api/mini-miner.ts index 4a4ef5daa..0bef1a819 100644 --- a/backend/src/api/mini-miner.ts +++ b/backend/src/api/mini-miner.ts @@ -337,7 +337,7 @@ export function makeBlockTemplate(candidates: MempoolTransactionExtended[], acce let failures = 0; while (mempoolArray.length || modified.length) { // skip invalid transactions - while (mempoolArray[0].used || mempoolArray[0].modified) { + while (mempoolArray[0]?.used || mempoolArray[0]?.modified) { mempoolArray.shift(); } diff --git a/backend/src/api/mining/mining-routes.ts b/backend/src/api/mining/mining-routes.ts index 08ea0d1bc..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,28 +456,24 @@ 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); } } private async $requestAcceleration(req: Request, res: Response): Promise { - if (config.MEMPOOL_SERVICES.ACCELERATIONS || config.MEMPOOL.OFFICIAL) { - res.status(405).send('not available.'); - return; - } res.setHeader('Pragma', 'no-cache'); res.setHeader('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'); res.setHeader('expires', -1); try { accelerationApi.accelerationRequested(req.params.txid); - res.status(200).send('ok'); + 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/mining/mining.ts b/backend/src/api/mining/mining.ts index 21ee4b35a..7e3ec525a 100644 --- a/backend/src/api/mining/mining.ts +++ b/backend/src/api/mining/mining.ts @@ -136,9 +136,13 @@ class Mining { poolsStatistics['blockCount'] = blockCount; const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h'); + const totalBlock3d: number = await BlocksRepository.$blockCount(null, '3d'); + const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w'); try { poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h); + poolsStatistics['lastEstimatedHashrate3d'] = await bitcoinClient.getNetworkHashPs(totalBlock3d); + poolsStatistics['lastEstimatedHashrate1w'] = await bitcoinClient.getNetworkHashPs(totalBlock1w); } catch (e) { poolsStatistics['lastEstimatedHashrate'] = 0; logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining); diff --git a/backend/src/api/prices/prices.routes.ts b/backend/src/api/prices/prices.routes.ts index b46331b73..e395fb44b 100644 --- a/backend/src/api/prices/prices.routes.ts +++ b/backend/src/api/prices/prices.routes.ts @@ -1,10 +1,15 @@ import { Application, Request, Response } from 'express'; import config from '../../config'; import pricesUpdater from '../../tasks/price-updater'; +import logger from '../../logger'; +import PricesRepository from '../../repositories/PricesRepository'; class PricesRoutes { public initRoutes(app: Application): void { - app.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this)); + app + .get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this)) + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/usd-price-history', this.$getAllPrices.bind(this)) + ; } private $getCurrentPrices(req: Request, res: Response): void { @@ -14,6 +19,23 @@ class PricesRoutes { res.json(pricesUpdater.getLatestPrices()); } + + private async $getAllPrices(req: Request, res: Response): Promise { + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 360_0000 / config.MEMPOOL.PRICE_UPDATES_PER_HOUR).toUTCString()); + + try { + const usdPriceHistory = await PricesRepository.$getPricesTimesAndId(); + const responseData = usdPriceHistory.map(p => { + return { time: p.time, USD: p.USD }; + }); + res.status(200).json(responseData); + } catch (e: any) { + logger.err(`Exception ${e} in PricesRoutes::$getAllPrices. Code: ${e.code}. Message: ${e.message}`); + res.status(403).send(); + } + } } export default new PricesRoutes(); 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 386c40b8e..053da6e82 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,13 +40,23 @@ 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 websocketPath = config.MEMPOOL_SERVICES?.API ? `${config.MEMPOOL_SERVICES.API.replace('https://', 'wss://').replace('http://', 'ws://')}/accelerator/ws` : '/'; + private _accelerations: Record = {}; private lastPoll = 0; + private lastPing = Date.now(); + private lastPong = Date.now(); private forcePoll = false; private myAccelerations: Record = {}; - public get accelerations(): Acceleration[] | null { + public constructor() {} + + public getAccelerations(): Record { return this._accelerations; } @@ -52,7 +65,9 @@ class AccelerationApi { } public accelerationRequested(txid: string): void { - this.myAccelerations[txid] = { status: 'requested', added: Date.now() }; + if (this.onDemandPollingEnabled) { + this.myAccelerations[txid] = { status: 'requested', added: Date.now() }; + } } public accelerationConfirmed(): void { @@ -69,11 +84,18 @@ class AccelerationApi { } } - public async $updateAccelerations(): Promise { - if (config.MEMPOOL_SERVICES.ACCELERATIONS) { + 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 { @@ -82,7 +104,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))); @@ -117,7 +139,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; } @@ -149,6 +175,148 @@ 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(this.websocketPath); + this.lastPing = 0; + + this.ws.on('open', () => { + logger.info(`Acceleration websocket opened to ${this.websocketPath}`); + this.websocketConnected = true; + this.ws?.send(JSON.stringify({ + 'watch-accelerations': true + })); + }); + + this.ws.on('error', (error) => { + let errMsg = `Acceleration websocket error on ${this.websocketPath}: ${error['code']}`; + if (error['errors']) { + errMsg += ' - ' + error['errors'].join(' - '); + } + logger.err(errMsg); + 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 msg = (isBinary ? data : data.toString()) as string; + const parsedMsg = msg?.length ? JSON.parse(msg) : null; + this.handleWebsocketMessage(parsedMsg); + } catch (e) { + logger.warn('Failed to parse acceleration websocket message: ' + (e instanceof Error ? e.message : e)); + } + }); + + this.ws.on('ping', () => { + logger.debug('received ping from acceleration websocket server'); + }); + + this.ws.on('pong', () => { + logger.debug('received pong from acceleration websocket server'); + this.lastPong = Date.now(); + }); + } else if (this.websocketConnected) { + if (this.lastPing && this.lastPing > this.lastPong && (Date.now() - this.lastPing > 10000)) { + logger.warn('No pong received within 10 seconds, terminating connection'); + try { + this.ws?.terminate(); + } catch (e) { + logger.warn('failed to terminate acceleration websocket connection: ' + (e instanceof Error ? e.message : e)); + } finally { + this.ws = null; + this.websocketConnected = false; + this.lastPing = 0; + } + } else if (!this.lastPing || (Date.now() - this.lastPing > 30000)) { + logger.debug('sending ping to acceleration websocket server'); + if (this.ws?.readyState === WebSocket.OPEN) { + try { + this.ws?.ping(); + this.lastPing = Date.now(); + } catch (e) { + logger.warn('failed to send ping to acceleration websocket server: ' + (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/wallets.ts b/backend/src/api/services/wallets.ts index b20087ead..30f35feff 100644 --- a/backend/src/api/services/wallets.ts +++ b/backend/src/api/services/wallets.ts @@ -8,7 +8,15 @@ import { TransactionExtended } from '../../mempool.interfaces'; interface WalletAddress { address: string; active: boolean; - transactions?: IEsploraApi.AddressTxSummary[]; + 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 WalletConfig { @@ -22,7 +30,7 @@ interface Wallet extends WalletConfig { lastPoll: number; } -const POLL_FREQUENCY = 60 * 60 * 1000; // 1 hour +const POLL_FREQUENCY = 5 * 60 * 1000; // 5 minutes class WalletApi { private wallets: Record = {}; @@ -39,8 +47,11 @@ class WalletApi { return this.wallets?.[wallet]?.addresses || {}; } - // resync wallet addresses from the provided API + // 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]; @@ -77,10 +88,14 @@ class WalletApi { const refreshTransactions = !wallet.addresses[address.address] || address.active; 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: await bitcoinApi.$getAddressTransactionSummary(address.address), + stats: addressInfo.chain_stats, + lastSync: Date.now(), }; logger.debug(`Synced ${walletAddress.transactions?.length || 0} transactions for wallet ${wallet.name} address ${address.address}`); wallet.addresses[address.address] = walletAddress; @@ -91,36 +106,61 @@ class WalletApi { } // 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> = {}; + 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] = {}; + 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 })) { - if (!walletTransactions[walletKey][address]) { - walletTransactions[walletKey][address] = []; - } - walletTransactions[walletKey][address].push({ + // 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) { + for (const address of Object.keys({ ...funded, ...spent })) { + if (!walletTransactions[walletKey][address]) { + walletTransactions[walletKey][address] = []; + } + walletTransactions[walletKey][address].push({ + txid: tx.txid, + value: (funded[address] ?? 0) - (spent[address] ?? 0), + height: block.height, + time: block.timestamp, + }); + } } } } @@ -128,4 +168,4 @@ class WalletApi { } } -export default new WalletApi(); \ No newline at end of file +export default new WalletApi(); diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts index b3077b935..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)); @@ -338,6 +339,87 @@ class TransactionUtils { const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2; return witness[positionOfScript]; } + + // calculate the most parsimonious set of prioritizations given a list of block transactions + // (i.e. the most likely prioritizations and deprioritizations) + public identifyPrioritizedTransactions(transactions: any[], rateKey: string): { prioritized: string[], deprioritized: string[] } { + // find the longest increasing subsequence of transactions + // (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms) + // should be O(n log n) + const X = transactions.slice(1).reverse().map((tx) => ({ txid: tx.txid, rate: tx[rateKey] })); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase) + if (X.length < 2) { + return { prioritized: [], deprioritized: [] }; + } + const N = X.length; + const P: number[] = new Array(N); + const M: number[] = new Array(N + 1); + M[0] = -1; // undefined so can be set to any value + + let L = 0; + for (let i = 0; i < N; i++) { + // Binary search for the smallest positive l ≤ L + // such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize + let lo = 1; + let hi = L + 1; + while (lo < hi) { + const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi + if (X[M[mid]].rate > X[i].rate) { + hi = mid; + } else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize + lo = mid + 1; + } + } + + // After searching, lo == hi is 1 greater than the + // length of the longest prefix of X[i] + const newL = lo; + + // The predecessor of X[i] is the last index of + // the subsequence of length newL-1 + P[i] = M[newL - 1]; + M[newL] = i; + + if (newL > L) { + // If we found a subsequence longer than any we've + // found yet, update L + L = newL; + } + } + + // Reconstruct the longest increasing subsequence + // It consists of the values of X at the L indices: + // ..., P[P[M[L]]], P[M[L]], M[L] + const LIS: any[] = new Array(L); + let k = M[L]; + for (let j = L - 1; j >= 0; j--) { + LIS[j] = X[k]; + k = P[k]; + } + + const lisMap = new Map(); + LIS.forEach((tx, index) => lisMap.set(tx.txid, index)); + + const prioritized: string[] = []; + const deprioritized: string[] = []; + + let lastRate = X[0].rate; + + for (const tx of X) { + if (lisMap.has(tx.txid)) { + lastRate = tx.rate; + } else { + if (Math.abs(tx.rate - lastRate) < 0.1) { + // skip if the rate is almost the same as the previous transaction + } else if (tx.rate <= lastRate) { + prioritized.push(tx.txid); + } else { + deprioritized.push(tx.txid); + } + } + } + + return { prioritized, deprioritized }; + } } export default new TransactionUtils(); diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 37f50fafc..13e27c360 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -3,7 +3,7 @@ import * as WebSocket from 'ws'; import { BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse, OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo, - MempoolBlockDelta, MempoolDelta, MempoolDeltaTxids + MempoolDelta, MempoolDeltaTxids } from '../mempool.interfaces'; import blocks from './blocks'; import memPool from './mempool'; @@ -16,11 +16,13 @@ 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'; @@ -35,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 = [ @@ -58,6 +61,8 @@ class WebsocketHandler { private lastRbfSummary: ReplacementInfo[] | null = null; private mempoolSequence: number = 0; + private accelerations: Record = {}; + constructor() { } addWebsocketServer(wss: WebSocket.Server) { @@ -493,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'); @@ -529,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'); @@ -538,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; @@ -556,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; @@ -587,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 }); } } @@ -666,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 @@ -832,6 +887,7 @@ class WebsocketHandler { accelerated: mempoolTx.acceleration || undefined, acceleratedBy: mempoolTx.acceleratedBy || undefined, acceleratedAt: mempoolTx.acceleratedAt || undefined, + feeDelta: mempoolTx.feeDelta || undefined, }, accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid), }; @@ -873,6 +929,7 @@ class WebsocketHandler { accelerated: mempoolTx.acceleration || undefined, acceleratedBy: mempoolTx.acceleratedBy || undefined, acceleratedAt: mempoolTx.acceleratedAt || undefined, + feeDelta: mempoolTx.feeDelta || undefined, }; if (!mempoolTx.cpfpChecked) { calculateMempoolTxCpfp(mempoolTx, newMempool); @@ -940,6 +997,8 @@ class WebsocketHandler { throw new Error('No WebSocket.Server have been set'); } + const blockTransactions = structuredClone(transactions); + this.printLogs(); await statistics.runStatistics(); @@ -949,10 +1008,10 @@ class WebsocketHandler { let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool); const accelerations = Object.values(mempool.getAccelerations()); - await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, transactions); + 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()) { @@ -969,7 +1028,7 @@ class WebsocketHandler { } if (Common.indexingEnabled()) { - const { censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool); + const { unseen, censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(block.height, blockTransactions, projectedBlocks, auditMempool); const matchRate = Math.round(score * 100 * 100) / 100; const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : []; @@ -991,9 +1050,11 @@ class WebsocketHandler { }); BlocksAuditsRepository.$saveAudit({ + version: 1, time: block.timestamp, height: block.height, hash: block.id, + unseenTxs: unseen, addedTxs: added, prioritizedTxs: prioritized, missingTxs: censored, @@ -1020,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 @@ -1150,6 +1219,7 @@ class WebsocketHandler { accelerated: mempoolTx.acceleration || undefined, acceleratedBy: mempoolTx.acceleratedBy || undefined, acceleratedAt: mempoolTx.acceleratedAt || undefined, + feeDelta: mempoolTx.feeDelta || undefined, }, accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid), }); @@ -1172,6 +1242,7 @@ class WebsocketHandler { accelerated: mempoolTx.acceleration || undefined, acceleratedBy: mempoolTx.acceleratedBy || undefined, acceleratedAt: mempoolTx.acceleratedAt || undefined, + feeDelta: mempoolTx.feeDelta || undefined, }; } } diff --git a/backend/src/config.ts b/backend/src/config.ts index e272d8d39..ee95be62d 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; @@ -200,8 +202,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, @@ -233,7 +236,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', diff --git a/backend/src/index.ts b/backend/src/index.ts index 0e43dbe31..d939b7423 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -213,6 +213,8 @@ class Server { } }); } + + poolsUpdater.$startService(); } async runMainUpdateLoop(): Promise { @@ -231,11 +233,11 @@ 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) { @@ -316,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); diff --git a/backend/src/indexer.ts b/backend/src/indexer.ts index 0dd1090b8..dfd7f1317 100644 --- a/backend/src/indexer.ts +++ b/backend/src/indexer.ts @@ -10,6 +10,7 @@ import config from './config'; import auditReplicator from './replication/AuditReplication'; import statisticsReplicator from './replication/StatisticsReplication'; import AccelerationRepository from './repositories/AccelerationRepository'; +import BlocksAuditsRepository from './repositories/BlocksAuditsRepository'; export interface CoreIndex { name: string; @@ -192,6 +193,7 @@ class Indexer { await auditReplicator.$sync(); await statisticsReplicator.$sync(); await AccelerationRepository.$indexPastAccelerations(); + await BlocksAuditsRepository.$migrateAuditsV0toV1(); // do not wait for classify blocks to finish blocks.$classifyBlocks(); } catch (e) { diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index 0ad60f4b9..dc703af21 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -29,9 +29,11 @@ export interface PoolStats extends PoolInfo { } export interface BlockAudit { + version: number, time: number, height: number, hash: string, + unseenTxs: string[], missingTxs: string[], freshTxs: string[], sigopTxs: string[], @@ -126,6 +128,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction { acceleration?: boolean; acceleratedBy?: number[]; acceleratedAt?: number; + feeDelta?: number; replacement?: boolean; uid?: number; flags?: number; @@ -296,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; @@ -316,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; @@ -382,8 +387,9 @@ export interface CpfpCluster { } export interface CpfpSummary { - transactions: TransactionExtended[]; + transactions: MempoolTransactionExtended[]; clusters: CpfpCluster[]; + version: number; } export interface Statistic { @@ -449,7 +455,7 @@ export interface OptimizedStatistic { export interface TxTrackingInfo { replacedBy?: string, - position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number }, + position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number, feeDelta?: number }, cpfp?: { ancestors?: Ancestor[], bestDescendant?: Ancestor | null, @@ -462,6 +468,7 @@ export interface TxTrackingInfo { accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number, + feeDelta?: number, confirmed?: boolean } diff --git a/backend/src/replication/AuditReplication.ts b/backend/src/replication/AuditReplication.ts index 4ea629839..6f616dbbe 100644 --- a/backend/src/replication/AuditReplication.ts +++ b/backend/src/replication/AuditReplication.ts @@ -31,11 +31,11 @@ class AuditReplication { const missingAudits = await this.$getMissingAuditBlocks(); logger.debug(`Fetching missing audit data for ${missingAudits.length} blocks from trusted servers`, 'Replication'); - + let totalSynced = 0; let totalMissed = 0; let loggerTimer = Date.now(); - // process missing audits in batches of + // process missing audits in batches of BATCH_SIZE for (let i = 0; i < missingAudits.length; i += BATCH_SIZE) { const slice = missingAudits.slice(i, i + BATCH_SIZE); const results = await Promise.all(slice.map(hash => this.$syncAudit(hash))); @@ -109,9 +109,11 @@ class AuditReplication { version: 1, }); await blocksAuditsRepository.$saveAudit({ + version: auditSummary.version || 0, hash: blockHash, height: auditSummary.height, time: auditSummary.timestamp || auditSummary.time, + unseenTxs: auditSummary.unseenTxs || [], missingTxs: auditSummary.missingTxs || [], addedTxs: auditSummary.addedTxs || [], prioritizedTxs: auditSummary.prioritizedTxs || [], diff --git a/backend/src/repositories/AccelerationRepository.ts b/backend/src/repositories/AccelerationRepository.ts index 70fa78dc6..4c9896296 100644 --- a/backend/src/repositories/AccelerationRepository.ts +++ b/backend/src/repositories/AccelerationRepository.ts @@ -192,6 +192,7 @@ class AccelerationRepository { } } + // modifies block transactions public async $indexAccelerationsForBlock(block: BlockExtended, accelerations: Acceleration[], transactions: MempoolTransactionExtended[]): Promise { const blockTxs: { [txid: string]: MempoolTransactionExtended } = {}; for (const tx of transactions) { diff --git a/backend/src/repositories/BlocksAuditsRepository.ts b/backend/src/repositories/BlocksAuditsRepository.ts index 1e0d28689..3b3f79ce0 100644 --- a/backend/src/repositories/BlocksAuditsRepository.ts +++ b/backend/src/repositories/BlocksAuditsRepository.ts @@ -1,13 +1,24 @@ -import blocks from '../api/blocks'; import DB from '../database'; import logger from '../logger'; -import { BlockAudit, AuditScore, TransactionAudit } from '../mempool.interfaces'; +import bitcoinApi from '../api/bitcoin/bitcoin-api-factory'; +import { BlockAudit, AuditScore, TransactionAudit, TransactionStripped } from '../mempool.interfaces'; + +interface MigrationAudit { + version: number, + height: number, + id: string, + timestamp: number, + prioritizedTxs: string[], + acceleratedTxs: string[], + template: TransactionStripped[], + transactions: TransactionStripped[], +} class BlocksAuditRepositories { public async $saveAudit(audit: BlockAudit): Promise { try { - await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight) - VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs), + await DB.query(`INSERT INTO blocks_audits(version, time, height, hash, unseen_txs, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight) + VALUE (?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.version, audit.time, audit.height, audit.hash, JSON.stringify(audit.unseenTxs), JSON.stringify(audit.missingTxs), JSON.stringify(audit.addedTxs), JSON.stringify(audit.prioritizedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]); } catch (e: any) { if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart @@ -62,24 +73,30 @@ class BlocksAuditRepositories { public async $getBlockAudit(hash: string): Promise { try { const [rows]: any[] = await DB.query( - `SELECT blocks_audits.height, blocks_audits.hash as id, UNIX_TIMESTAMP(blocks_audits.time) as timestamp, - template, - missing_txs as missingTxs, - added_txs as addedTxs, - prioritized_txs as prioritizedTxs, - fresh_txs as freshTxs, - sigop_txs as sigopTxs, - fullrbf_txs as fullrbfTxs, - accelerated_txs as acceleratedTxs, - match_rate as matchRate, - expected_fees as expectedFees, - expected_weight as expectedWeight + `SELECT + blocks_audits.version, + blocks_audits.height, + blocks_audits.hash as id, + UNIX_TIMESTAMP(blocks_audits.time) as timestamp, + template, + unseen_txs as unseenTxs, + missing_txs as missingTxs, + added_txs as addedTxs, + prioritized_txs as prioritizedTxs, + fresh_txs as freshTxs, + sigop_txs as sigopTxs, + fullrbf_txs as fullrbfTxs, + accelerated_txs as acceleratedTxs, + match_rate as matchRate, + expected_fees as expectedFees, + expected_weight as expectedWeight FROM blocks_audits JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash WHERE blocks_audits.hash = ? `, [hash]); if (rows.length) { + rows[0].unseenTxs = JSON.parse(rows[0].unseenTxs); rows[0].missingTxs = JSON.parse(rows[0].missingTxs); rows[0].addedTxs = JSON.parse(rows[0].addedTxs); rows[0].prioritizedTxs = JSON.parse(rows[0].prioritizedTxs); @@ -101,7 +118,7 @@ class BlocksAuditRepositories { public async $getBlockTxAudit(hash: string, txid: string): Promise { try { const blockAudit = await this.$getBlockAudit(hash); - + if (blockAudit) { const isAdded = blockAudit.addedTxs.includes(txid); const isPrioritized = blockAudit.prioritizedTxs.includes(txid); @@ -115,16 +132,17 @@ class BlocksAuditRepositories { firstSeen = tx.time; } }); + const wasSeen = blockAudit.version === 1 ? !blockAudit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated); return { - seen: isExpected || isPrioritized || isAccelerated, + seen: wasSeen, expected: isExpected, - added: isAdded, + added: isAdded && (blockAudit.version === 0 || !wasSeen), prioritized: isPrioritized, conflict: isConflict, accelerated: isAccelerated, firstSeen, - } + }; } return null; } catch (e: any) { @@ -186,6 +204,96 @@ class BlocksAuditRepositories { throw e; } } + + /** + * [INDEXING] Migrate audits from v0 to v1 + */ + public async $migrateAuditsV0toV1(): Promise { + try { + let done = false; + let processed = 0; + let lastHeight; + while (!done) { + const [toMigrate]: MigrationAudit[][] = await DB.query( + `SELECT + blocks_audits.height as height, + blocks_audits.hash as id, + UNIX_TIMESTAMP(blocks_audits.time) as timestamp, + blocks_summaries.transactions as transactions, + blocks_templates.template as template, + blocks_audits.prioritized_txs as prioritizedTxs, + blocks_audits.accelerated_txs as acceleratedTxs + FROM blocks_audits + JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash + JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash + WHERE blocks_audits.version = 0 + AND blocks_summaries.version = 2 + ORDER BY blocks_audits.height DESC + LIMIT 100 + `) as any[]; + + if (toMigrate.length <= 0 || lastHeight === toMigrate[0].height) { + done = true; + break; + } + lastHeight = toMigrate[0].height; + + logger.info(`migrating ${toMigrate.length} audits to version 1`); + + for (const audit of toMigrate) { + // unpack JSON-serialized transaction lists + audit.transactions = JSON.parse((audit.transactions as any as string) || '[]'); + audit.template = JSON.parse((audit.template as any as string) || '[]'); + + // we know transactions in the template, or marked "prioritized" or "accelerated" + // were seen in our mempool before the block was mined. + const isSeen = new Set(); + for (const tx of audit.template) { + isSeen.add(tx.txid); + } + for (const txid of audit.prioritizedTxs) { + isSeen.add(txid); + } + for (const txid of audit.acceleratedTxs) { + isSeen.add(txid); + } + const unseenTxs = audit.transactions.slice(0).map(tx => tx.txid).filter(txid => !isSeen.has(txid)); + + // identify "prioritized" transactions + const prioritizedTxs: string[] = []; + let lastEffectiveRate = 0; + // Iterate over the mined template from bottom to top (excluding the coinbase) + // Transactions should appear in ascending order of mining priority. + for (let i = audit.transactions.length - 1; i > 0; i--) { + const blockTx = audit.transactions[i]; + // If a tx has a lower in-band effective fee rate than the previous tx, + // it must have been prioritized out-of-band (in order to have a higher mining priority) + // so exclude from the analysis. + if ((blockTx.rate || 0) < lastEffectiveRate) { + prioritizedTxs.push(blockTx.txid); + } else { + lastEffectiveRate = blockTx.rate || 0; + } + } + + // Update audit in the database + await DB.query(` + UPDATE blocks_audits SET + version = ?, + unseen_txs = ?, + prioritized_txs = ? + WHERE hash = ? + `, [1, JSON.stringify(unseenTxs), JSON.stringify(prioritizedTxs), audit.id]); + } + + processed += toMigrate.length; + } + + logger.info(`migrated ${processed} audits to version 1`); + } catch (e: any) { + logger.err(`Error while migrating audits from v0 to v1. Will try again later. Reason: ` + (e instanceof Error ? e.message : e)); + } + } } export default new BlocksAuditRepositories(); diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 90100a767..424a668c7 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 { @@ -498,7 +501,7 @@ class BlocksRepository { } query += ` ORDER BY height DESC - LIMIT 10`; + LIMIT 100`; try { const [rows]: any[] = await DB.query(query, params); @@ -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 @@ -1106,7 +1129,7 @@ class BlocksRepository { let summaryVersion = 0; if (config.MEMPOOL.BACKEND === 'esplora') { const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx)); - summary = blocks.summarizeBlockTransactions(dbBlk.id, txs); + summary = blocks.summarizeBlockTransactions(dbBlk.id, dbBlk.height, txs); summaryVersion = 1; } else { // Call Core RPC @@ -1123,6 +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 3414e8269..f9755fcb4 100644 --- a/backend/src/utils/bitcoin-script.ts +++ b/backend/src/utils/bitcoin-script.ts @@ -158,7 +158,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb if (!opN) { return; } - if (!opN.startsWith('OP_PUSHNUM_')) { + if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) { return; } const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10); @@ -178,7 +178,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb if (!opM) { return; } - if (!opM.startsWith('OP_PUSHNUM_')) { + if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) { return; } const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10); @@ -200,4 +200,28 @@ export function getVarIntLength(n: number): number { } else { return 9; } +} + +/** Extracts miner names from a DATUM coinbase transaction */ +export function parseDATUMTemplateCreator(coinbaseRaw: string): string[] | null { + let bytes: number[] = []; + for (let c = 0; c < coinbaseRaw.length; c += 2) { + bytes.push(parseInt(coinbaseRaw.slice(c, c + 2), 16)); + } + + // Skip block height + let tagLengthByte = 1 + bytes[0]; + + let tagsLength = bytes[tagLengthByte]; + if (tagsLength == 0x4c) { + tagLengthByte += 1; + tagsLength = bytes[tagLengthByte]; + } + + const tagStart = tagLengthByte + 1; + const tags = bytes.slice(tagStart, tagStart + tagsLength); + let tagString = String.fromCharCode(...tags); + tagString = tagString.replace('\x00', ''); + + return tagString.split('\x0f').map((name) => name.replace(/[^a-zA-Z0-9 ]/g, '')); } \ 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 daab09990..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} @@ -144,7 +146,7 @@ __REPLICATION_STATISTICS_START_TIME__=${REPLICATION_STATISTICS_START_TIME:=14819 __REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]} # MEMPOOL_SERVICES -__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""} +__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"} __MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false} # REDIS @@ -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/docker/frontend/entrypoint.sh b/docker/frontend/entrypoint.sh index 20b391087..2086188c9 100644 --- a/docker/frontend/entrypoint.sh +++ b/docker/frontend/entrypoint.sh @@ -41,7 +41,7 @@ __TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0} __SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0} __ACCELERATOR__=${ACCELERATOR:=false} __ACCELERATOR_BUTTON__=${ACCELERATOR_BUTTON:=true} -__SERVICES_API__=${SERVICES_API:=false} +__SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services} __PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false} __HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true} __ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false} diff --git a/frontend/README.md b/frontend/README.md index 069f1d5f0..fb2a5e291 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -33,7 +33,7 @@ $ npm run config:defaults:liquid ### 3. Run the Frontend -_Make sure to use Node.js 16.10 and npm 7._ +_Make sure to use Node.js 20.x and npm 9.x or newer._ Install project dependencies and run the frontend server: @@ -70,7 +70,7 @@ Set up the [Mempool backend](../backend/) first, if you haven't already. ### 1. Build the Frontend -_Make sure to use Node.js 16.10 and npm 7._ +_Make sure to use Node.js 20.x and npm 9.x or newer._ Build the frontend: diff --git a/frontend/angular.json b/frontend/angular.json index 190982225..3aa1cb6a8 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -54,6 +54,10 @@ "translation": "src/locale/messages.fr.xlf", "baseHref": "/fr/" }, + "hr": { + "translation": "src/locale/messages.hr.xlf", + "baseHref": "/hr/" + }, "ja": { "translation": "src/locale/messages.ja.xlf", "baseHref": "/ja/" diff --git a/frontend/custom-meta-config.json b/frontend/custom-meta-config.json new file mode 100644 index 000000000..6fa46192a --- /dev/null +++ b/frontend/custom-meta-config.json @@ -0,0 +1,51 @@ +{ + "theme": "contrast", + "enterprise": "meta", + "branding": { + "name": "metaplanet", + "title": "Metaplanet", + "site_id": 21, + "header_img": "/resources/metalogo.svg", + "footer_img": "/resources/metalogo.svg" + }, + "dashboard": { + "widgets": [ + { + "component": "fees", + "mobileOrder": 4 + }, + { + "component": "walletBalance", + "mobileOrder": 1, + "props": { + "wallet": "3350" + } + }, + { + "component": "twitter", + "mobileOrder": 5, + "props": { + "handle": "Metaplanet_JP" + } + }, + { + "component": "wallet", + "mobileOrder": 2, + "props": { + "wallet": "3350", + "period": "all" + } + }, + { + "component": "blocks" + }, + { + "component": "walletTransactions", + "mobileOrder": 3, + "props": { + "wallet": "3350" + } + } + ] + } +} \ No newline at end of file diff --git a/frontend/cypress/e2e/mainnet/mainnet.spec.ts b/frontend/cypress/e2e/mainnet/mainnet.spec.ts index 4403949b6..a1082b769 100644 --- a/frontend/cypress/e2e/mainnet/mainnet.spec.ts +++ b/frontend/cypress/e2e/mainnet/mainnet.spec.ts @@ -543,16 +543,7 @@ describe('Mainnet', () => { } }); - cy.get('.alert').should('be.visible'); - cy.get('.alert').invoke('css', 'width').then((alertWidth) => { - cy.get('.container-xl > :nth-child(3)').invoke('css', 'width').should('equal', alertWidth); - }); - - cy.get('.btn-warning').then(getRectangle).then((rectA) => { - cy.get('.alert').then(getRectangle).then((rectB) => { - expect(areOverlapping(rectA, rectB), 'Confirmations box and RBF alert are overlapping').to.be.false; - }); - }); + cy.get('.alert-replaced').should('be.visible'); }); it('shows RBF transactions properly (desktop)', () => { diff --git a/frontend/cypress/fixtures/mainnet_mempoolInfo.json b/frontend/cypress/fixtures/mainnet_mempoolInfo.json index 5c41bd4dd..584364e9a 100644 --- a/frontend/cypress/fixtures/mainnet_mempoolInfo.json +++ b/frontend/cypress/fixtures/mainnet_mempoolInfo.json @@ -750,7 +750,7 @@ }, "backendInfo": { "hostname": "node205.tk7.mempool.space", - "version": "3.0.0-dev", + "version": "3.1.0-dev", "gitCommit": "abbc8a134", "lightning": false }, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d164bd869..a27bffcb4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mempool-frontend", - "version": "3.0.0-dev", + "version": "3.1.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mempool-frontend", - "version": "3.0.0-dev", + "version": "3.1.0-dev", "license": "GNU Affero General Public License v3.0", "dependencies": { "@angular-devkit/build-angular": "^17.3.1", @@ -34,7 +34,7 @@ "clipboard": "^2.0.11", "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", @@ -42,7 +42,7 @@ "rxjs": "~7.8.1", "tinyify": "^4.0.0", "tlite": "^0.1.9", - "tslib": "~2.6.0", + "tslib": "~2.8.0", "zone.js": "~0.14.4" }, "devDependencies": { @@ -51,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", @@ -62,7 +62,7 @@ "optionalDependencies": { "@cypress/schematic": "^2.5.0", "@types/cypress": "^1.1.3", - "cypress": "^13.13.0", + "cypress": "^13.15.0", "cypress-fail-on-console-error": "~5.1.0", "cypress-wait-until": "^2.0.1", "mock-socket": "~9.3.1", @@ -699,6 +699,11 @@ "node": ">=10" } }, + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/@angular-devkit/build-webpack": { "version": "0.1703.1", "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.1.tgz", @@ -3108,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", @@ -3119,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", @@ -3196,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" ], @@ -3211,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" ], @@ -3226,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" ], @@ -3241,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" ], @@ -3256,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" ], @@ -3271,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" ], @@ -3286,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" ], @@ -3301,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" ], @@ -3316,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" ], @@ -3331,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" ], @@ -3346,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" ], @@ -3361,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" ], @@ -3376,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" ], @@ -3391,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" ], @@ -3406,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" ], @@ -3421,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" ], @@ -3436,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" ], @@ -3451,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" ], @@ -3466,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" ], @@ -3481,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" ], @@ -3496,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" ], @@ -3511,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" ], @@ -3526,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" ], @@ -3541,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" ], @@ -4308,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" ], @@ -4320,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" ], @@ -4332,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" ], @@ -4344,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" ], @@ -4356,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" ], @@ -4368,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" ], @@ -4380,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" ], @@ -4391,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" ], @@ -4403,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" ], @@ -4416,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" ], @@ -4428,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" ], @@ -4440,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" ], @@ -4452,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" ], @@ -4759,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": "*" @@ -4796,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", @@ -5792,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": { @@ -6014,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", @@ -6026,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" @@ -6060,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", @@ -6182,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", @@ -6202,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", @@ -6224,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", @@ -6238,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", @@ -6385,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", @@ -6429,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", @@ -6459,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", @@ -7668,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" } @@ -8040,13 +7953,13 @@ "peer": true }, "node_modules/cypress": { - "version": "13.13.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.0.tgz", - "integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==", + "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.0", + "@cypress/request": "^3.0.4", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", @@ -8805,9 +8718,9 @@ "integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==" }, "node_modules/elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "version": "6.5.7", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz", + "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==", "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -8879,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", @@ -8889,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", @@ -9205,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" @@ -9216,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": { @@ -9870,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", @@ -9918,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", @@ -9934,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", @@ -10172,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", @@ -10196,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", @@ -10335,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": { @@ -10987,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" @@ -12662,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", @@ -12688,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" @@ -13382,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" @@ -13669,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" } @@ -14185,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", @@ -14761,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" @@ -15222,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" @@ -15236,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" } }, @@ -15472,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", @@ -15613,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", @@ -15717,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" @@ -15895,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": { @@ -16161,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", @@ -16757,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", @@ -16925,9 +16789,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "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", @@ -17821,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", @@ -18298,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" }, @@ -18326,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" @@ -18849,6 +18699,11 @@ "requires": { "lru-cache": "^6.0.0" } + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, @@ -20493,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", @@ -20504,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", @@ -20572,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": { @@ -21256,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": { @@ -21598,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": "*" @@ -21634,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", @@ -22396,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": { @@ -22572,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", @@ -22584,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" @@ -22610,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" - } } } }, @@ -22707,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", @@ -22727,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", @@ -22787,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", @@ -22825,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", @@ -22852,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", @@ -22971,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", @@ -22982,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", @@ -23854,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", @@ -24127,12 +23896,12 @@ "peer": true }, "cypress": { - "version": "13.13.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.0.tgz", - "integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==", + "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.0", + "@cypress/request": "^3.0.4", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", @@ -24723,9 +24492,9 @@ "integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==" }, "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "version": "6.5.7", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz", + "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==", "requires": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -24792,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", @@ -24802,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": { @@ -25044,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": { @@ -25540,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", @@ -25585,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", @@ -25598,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", @@ -25778,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", @@ -25799,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", @@ -25892,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" } }, @@ -26360,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": { @@ -27591,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", @@ -27611,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": { @@ -28156,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" @@ -28364,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", @@ -28740,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", @@ -29137,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": { @@ -29495,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" } }, @@ -29663,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", @@ -29786,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": { @@ -29869,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": { @@ -29993,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" } }, @@ -30206,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", @@ -30654,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", @@ -30763,9 +30514,9 @@ } }, "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "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", @@ -31277,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", @@ -31612,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": { @@ -31623,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 59f0eb39e..6a0d7dc12 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mempool-frontend", - "version": "3.0.0-dev", + "version": "3.1.0-dev", "description": "Bitcoin mempool visualizer and blockchain explorer backend", "license": "GNU Affero General Public License v3.0", "homepage": "https://mempool.space", @@ -92,10 +92,10 @@ "ngx-infinite-scroll": "^17.0.0", "qrcode": "1.5.1", "rxjs": "~7.8.1", - "esbuild": "^0.23.0", + "esbuild": "^0.24.0", "tinyify": "^4.0.0", "tlite": "^0.1.9", - "tslib": "~2.6.0", + "tslib": "~2.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.13.0", + "cypress": "^13.15.0", "cypress-fail-on-console-error": "~5.1.0", "cypress-wait-until": "^2.0.1", "mock-socket": "~9.3.1", diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 8e996953d..d1748312d 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -1,14 +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 { 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 @@ -21,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: { @@ -44,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 }, }, { @@ -59,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 }, }, { @@ -82,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 }, }, { @@ -102,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: { @@ -125,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 }, }, { @@ -137,21 +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), + }, { path: '', - loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), + loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule), data: { preload: true }, }, { - path: 'tracker', - data: { networkSpecific: true }, - loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule), - }, - { - path: 'wallet', + path: 'widget/wallet', children: [], component: AddressGroupComponent, data: { @@ -163,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) }, ], }, @@ -210,13 +212,9 @@ 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 }, }, - { - path: '**', - redirectTo: '' - }, ]; if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { @@ -227,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: { @@ -250,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 }, }, { @@ -262,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: { @@ -283,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) }, ], }, @@ -298,16 +296,19 @@ 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 }, }, - { - path: '**', - redirectTo: '' - }, ]; } +if (!window['isMempoolSpaceBuild']) { + routes.push({ + path: '**', + redirectTo: '' + }); +} + @NgModule({ imports: [RouterModule.forRoot(routes, { initialNavigation: 'enabledBlocking', diff --git a/frontend/src/app/app.constants.ts b/frontend/src/app/app.constants.ts index aaa53b8ba..cef630984 100644 --- a/frontend/src/app/app.constants.ts +++ b/frontend/src/app/app.constants.ts @@ -151,7 +151,7 @@ export const languages: Language[] = [ { code: 'fr', name: 'Français' }, // French // { code: 'gl', name: 'Galego' }, // Galician { code: 'ko', name: '한국어' }, // Korean -// { code: 'hr', name: 'Hrvatski' }, // Croatian + { code: 'hr', name: 'Hrvatski' }, // Croatian // { code: 'id', name: 'Bahasa Indonesia' },// Indonesian { code: 'hi', name: 'हिन्दी' }, // Hindi { code: 'ne', name: 'नेपाली' }, // Nepalese 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 92d3de7f3..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 @@ -135,7 +135,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb return; } const opN = ops.pop(); - if (!opN.startsWith('OP_PUSHNUM_')) { + if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) { return; } const n = parseInt(opN.match(/[0-9]+/)[0], 10); @@ -152,7 +152,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb } } const opM = ops.pop(); - if (!opM.startsWith('OP_PUSHNUM_')) { + if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) { return; } const m = parseInt(opM.match(/[0-9]+/)[0], 10); @@ -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 41c0ce47f..40d6e1914 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -53,7 +53,7 @@ Spiral - + Blockstream - + + + Unchained - - - - - - - - Gemini + + + Bitkey Bull Bitcoin - + @@ -191,12 +188,30 @@ Exodus + + + + + + + + Gemini + + + + Leather + + + + + Taproot Wizards +
-
+

Whale Sponsors

@@ -435,7 +450,7 @@ Trademark Notice

- The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries. + The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.

While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our Trademark Policy and Guidelines for more details, published on <https://mempool.space/trademark-policy>. diff --git a/frontend/src/app/components/about/about.component.scss b/frontend/src/app/components/about/about.component.scss index a360e180c..6a76bf299 100644 --- a/frontend/src/app/components/about/about.component.scss +++ b/frontend/src/app/components/about/about.component.scss @@ -13,8 +13,6 @@ .image.not-rounded { border-radius: 0; - width: 60px; - height: 60px; } .intro { @@ -94,6 +92,13 @@ } } + .whale-sponsor { + img { + width: 70px; + height: 70px; + } + } + .alliances { margin-bottom: 100px; a { @@ -158,9 +163,8 @@ margin: 40px 29px 10px; &.image.coldcard { border-radius: 0; - width: auto; - max-height: 50px; - margin: 40px 29px 14px 29px; + height: auto; + margin: 20px 29px 20px; } } } @@ -254,3 +258,12 @@ width: 64px; height: 64px; } + +.enterprise-sponsor { + .wrapper { + display: flex; + flex-wrap: wrap; + justify-content: center; + max-width: 800px; + } +} \ No newline at end of file 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.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 82391dc89..df67de65c 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -389,21 +389,29 @@

}
- @if (canPayWithCashapp || canPayWithApplePay) { + @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {

OR

} } - @if (canPayWithCashapp || canPayWithApplePay) { + @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {

Pay  with

@if (canPayWithCashapp) { } @if (canPayWithApplePay) { - @if (canPayWithCashapp) {
} - + @if (canPayWithCashapp) { } +
+ +
+ } + @if (canPayWithGooglePay) { + @if (canPayWithCashapp || canPayWithApplePay) { } +
+ +
}
} @@ -427,7 +435,7 @@
- } @else if (step === 'cashapp' || step === 'applepay') { + } @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay') {
@@ -443,7 +451,7 @@
- @if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay) { + @if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay) {
@@ -463,11 +471,13 @@
@if (step === 'applepay') { -
+
} @else if (step === 'cashapp') { -
+
+ } @else if (step === 'googlepay') { +
} - @if (loadingCashapp || loadingApplePay) { + @if (loadingCashapp || loadingApplePay || loadingGooglePay) {
Loading payment method...
@@ -515,7 +525,7 @@
Confirming your acceleration with our mining pool partners... - @if (timeSincePaid > 20000) { + @if (timeSincePaid > 30000) { ...sorry, this is taking longer than expected... }
diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss index b35308384..ad085ed20 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss @@ -172,10 +172,6 @@ background-color: var(--tertiary); } -.btn-small-height { - line-height: 1; -} - .summary-row { display: flex; flex-direction: row; 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 71c46e2da..1a5ace34f 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -1,17 +1,19 @@ +/* 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, nextRoundNumber, 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 } 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'; +export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay'; export type AccelerationEstimate = { hasAccess: boolean; @@ -24,7 +26,7 @@ export type AccelerationEstimate = { mempoolBaseFee: number; vsizeFee: number; pools: number[]; - availablePaymentMethods: {[method: string]: {min: number, max: number}}; + availablePaymentMethods: Record; unavailable?: boolean; options: { // recommended bid options fee: number; // recommended userBid in sats @@ -47,7 +49,7 @@ export const MIN_BID_RATIO = 1; export const DEFAULT_BID_RATIO = 2; export const MAX_BID_RATIO = 4; -type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'processing' | 'paid' | 'success'; +type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'processing' | 'paid' | 'success'; @Component({ selector: 'app-accelerate-checkout', @@ -62,6 +64,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Input() scrollEvent: boolean; @Input() cashappEnabled: boolean = true; @Input() applePayEnabled: boolean = false; + @Input() googlePayEnabled: boolean = true; @Input() advancedEnabled: boolean = false; @Input() forceMobile: boolean = false; @Input() showDetails: boolean = false; @@ -72,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 @@ -80,18 +84,16 @@ export class AccelerateCheckout implements OnInit, OnDestroy { timePaid: number = 0; // time acceleration requested math = Math; isMobile: boolean = window.innerWidth <= 767.98; + isProdDomain = false; private _step: CheckoutStep = 'summary'; simpleMode: boolean = true; - paymentMethod: 'cashapp' | 'btcpay'; timeoutTimer: any; authSubscription$: Subscription; auth: IAuth | null = null; // accelerator stuff - square: { appId: string, locationId: string}; - accelerationUUID: string; accelerationSubscription: Subscription; difficultySubscription: Subscription; estimateSubscription: Subscription; @@ -112,14 +114,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy { // square loadingCashapp = false; loadingApplePay = false; - cashappError = false; - cashappSubmit: any; + loadingGooglePay = false; payments: any; cashAppPay: any; applePay: any; + googlePay: any; conversionsSubscription: Subscription; - conversions: any; - + conversions: Record; + // btcpay loadingBtcpayInvoice = false; invoice = undefined; @@ -134,16 +136,16 @@ export class AccelerateCheckout implements OnInit, OnDestroy { private authService: AuthServiceMempool, private enterpriseService: EnterpriseService, ) { - this.accelerationUUID = insecureRandomUUID(); + this.isProdDomain = this.stateService.env.PROD_DOMAINS.indexOf(document.location.hostname) > -1; // Check if Apple Pay available - // @ts-ignore https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview - if (window.ApplePaySession) { + // https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview + if (window['ApplePaySession']) { this.applePayEnabled = true; } } - ngOnInit() { + ngOnInit(): void { this.authSubscription$ = this.authService.getAuth$().subscribe((auth) => { if (this.auth?.user?.userId !== auth?.user?.userId) { this.auth = auth; @@ -168,13 +170,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.moveToStep('summary'); } - this.servicesApiService.setupSquare$().subscribe(ids => { - this.square = { - appId: ids.squareAppId, - locationId: ids.squareLocationId - }; - }); - this.conversionsSubscription = this.stateService.conversions$.subscribe( async (conversions) => { this.conversions = conversions; @@ -182,7 +177,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { ); } - ngOnDestroy() { + ngOnDestroy(): void { if (this.estimateSubscription) { this.estimateSubscription.unsubscribe(); } @@ -195,14 +190,17 @@ 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(); } } } - moveToStep(step: CheckoutStep) { + moveToStep(step: CheckoutStep): void { + this.processing = false; this._step = step; if (this.timeoutTimer) { clearTimeout(this.timeoutTimer); @@ -211,6 +209,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.fetchEstimate(); } if (this._step === 'checkout') { + this.insertSquare(); this.enterpriseService.goal(8); } if (this._step === 'checkout' && this.canPayWithBitcoin) { @@ -220,12 +219,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.requestBTCPayInvoice(); } else if (this._step === 'cashapp' && this.cashappEnabled) { this.loadingCashapp = true; - this.insertSquare(); this.setupSquare(); this.scrollToElementWithTimeout('confirm-title', 'center', 100); } else if (this._step === 'applepay' && this.applePayEnabled) { this.loadingApplePay = true; - this.insertSquare(); + this.setupSquare(); + this.scrollToElementWithTimeout('confirm-title', 'center', 100); + } else if (this._step === 'googlepay' && this.googlePayEnabled) { + this.loadingGooglePay = true; this.setupSquare(); this.scrollToElementWithTimeout('confirm-title', 'center', 100); } else if (this._step === 'paid') { @@ -234,7 +235,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { if (this.step === 'paid') { this.accelerateError = 'internal_server_error'; } - }, 120000) + }, 120000); } this.hasDetails.emit(this._step === 'quote'); } @@ -252,7 +253,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.scrollToElement(id, position); }, timeout); } - scrollToElement(id: string, position: ScrollLogicalPosition) { + scrollToElement(id: string, position: ScrollLogicalPosition): void { const acceleratePreviewAnchor = document.getElementById(id); if (acceleratePreviewAnchor) { this.cd.markForCheck(); @@ -267,7 +268,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { /** * Accelerator */ - fetchEstimate() { + fetchEstimate(): void { if (this.estimateSubscription) { this.estimateSubscription.unsubscribe(); } @@ -331,7 +332,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } }), - catchError((response) => { + catchError(() => { this.estimate = undefined; this.quoteError = `cannot_accelerate_tx`; this.estimateSubscription.unsubscribe(); @@ -367,6 +368,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(); } } @@ -374,18 +376,19 @@ 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(); } this.accelerationSubscription = this.servicesApiService.accelerate$( this.tx.txid, this.userBid, - this.accelerationUUID ).subscribe({ next: () => { + this.processing = false; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); this.showSuccess = true; @@ -393,6 +396,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.moveToStep('paid'); }, error: (response) => { + this.processing = false; this.accelerateError = response.error; } }); @@ -402,63 +406,74 @@ export class AccelerateCheckout implements OnInit, OnDestroy { * Square */ insertSquare(): void { - //@ts-ignore - if (window.Square) { + if (!this.isProdDomain && !isDevMode()) { + return; + } + if (window['Square']) { return; } let statsUrl = 'https://sandbox.web.squarecdn.com/v1/square.js'; - if (document.location.hostname === 'mempool-staging.fmt.mempool.space' || - document.location.hostname === 'mempool-staging.va1.mempool.space' || - document.location.hostname === 'mempool-staging.fra.mempool.space' || - document.location.hostname === 'mempool-staging.tk7.mempool.space' || - document.location.hostname === 'mempool.space') { - statsUrl = 'https://web.squarecdn.com/v1/square.js'; + if (this.isProdDomain) { + statsUrl = '/square/v1/square.js'; } - (function() { + (function(): void { const d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; - // @ts-ignore g.type='text/javascript'; g.src=statsUrl; s.parentNode.insertBefore(g, s); })(); } - setupSquare() { - const init = () => { + setupSquare(): void { + if (!this.isProdDomain && !isDevMode()) { + return; + } + const init = (): void => { this.initSquare(); }; - //@ts-ignore - if (!window.Square) { - console.debug('Square.js failed to load properly. Retrying in 1 second.'); - setTimeout(init, 1000); + if (!window['Square']) { + console.debug('Square.js failed to load properly. Retrying.'); + setTimeout(this.setupSquare.bind(this), 100); } else { init(); } } async initSquare(): Promise { try { - //@ts-ignore - this.payments = window.Square.payments(this.square.appId, this.square.locationId) - const urlParams = new URLSearchParams(window.location.search); - if (this._step === 'cashapp' || urlParams.get('cash_request_id')) { - await this.requestCashAppPayment(); - } else if (this._step === 'applepay') { - await this.requestApplePayPayment(); - } + this.servicesApiService.setupSquare$().subscribe({ + next: async (ids) => { + this.payments = window['Square'].payments(ids.squareAppId, ids.squareLocationId); + const urlParams = new URLSearchParams(window.location.search); + if (this._step === 'cashapp' || urlParams.get('cash_request_id')) { + await this.requestCashAppPayment(); + } else if (this._step === 'applepay') { + await this.requestApplePayPayment(); + } else if (this._step === 'googlepay') { + await this.requestGooglePayPayment(); + } + }, + error: () => { + console.debug('Error loading Square Payments'); + this.accelerateError = 'cannot_setup_square'; + } + }); } catch (e) { console.debug('Error loading Square Payments', e); - this.cashappError = true; - return; + this.accelerateError = 'cannot_setup_square'; } } /** * APPLE PAY */ - async requestApplePayPayment() { + 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; @@ -483,6 +498,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; @@ -494,6 +510,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()); @@ -502,9 +519,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 + costUSD ).subscribe({ next: () => { + this.processing = false; + this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); if (this.applePay) { this.applePay.destroy(); @@ -514,6 +533,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(() => { @@ -525,6 +545,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( @@ -535,6 +556,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } }); } catch (e) { + this.processing = false; console.error(e); } } @@ -542,13 +564,112 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } /** - * CASHAPP + * GOOGLE PAY */ - async requestCashAppPayment() { + 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; + if (this.googlePay) { + this.googlePay.destroy(); + } + + const costUSD = this.cost / 100_000_000 * conversions.USD; + const paymentRequest = this.payments.paymentRequest({ + countryCode: 'US', + currencyCode: 'USD', + total: { + amount: costUSD.toFixed(2), + label: 'Total' + } + }); + this.googlePay = await this.payments.googlePay(paymentRequest , { + referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, + }); + + await this.googlePay.attach(`#google-pay-button`, { + buttonType: 'pay', + buttonSizeMode: 'fill', + }); + this.loadingGooglePay = false; + + document.getElementById('google-pay-button').addEventListener('click', async event => { + event.preventDefault(); + const tokenResult = await this.googlePay.tokenize(); + if (tokenResult?.status === 'OK') { + const card = tokenResult.details?.card; + 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()); + this.servicesApiService.accelerateWithGooglePay$( + this.tx.txid, + tokenResult.token, + cardTag, + `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, + costUSD + ).subscribe({ + next: () => { + this.processing = false; + this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); + this.audioService.playSound('ascend-chime-cartoon'); + if (this.googlePay) { + this.googlePay.destroy(); + } + setTimeout(() => { + this.moveToStep('paid'); + }, 1000); + }, + error: (response) => { + this.processing = false; + this.accelerateError = response.error; + if (!(response.status === 403 && response.error === 'not_available')) { + setTimeout(() => { + // Reset everything by reloading the page :D, can be improved + const urlParams = new URLSearchParams(window.location.search); + window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``)); + }, 3000); + } + } + }); + } else { + this.processing = false; + let errorMessage = `Tokenization failed with status: ${tokenResult.status}`; + if (tokenResult.errors) { + errorMessage += ` and errors: ${JSON.stringify( + tokenResult.errors, + )}`; + } + throw new Error(errorMessage); + } + }); + } + ); + } + + /** + * 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; @@ -565,24 +686,21 @@ export class AccelerateCheckout implements OnInit, OnDestroy { amount: costUSD.toFixed(2), label: 'Total', pending: true, - productUrl: `${redirectHostname}/tracker/${this.tx.txid}`, - }, - button: { shape: 'semiround', size: 'small', theme: 'light'} + productUrl: `${redirectHostname}/tx/${this.tx.txid}`, + } }); this.cashAppPay = await this.payments.cashAppPay(paymentRequest, { - redirectURL: `${redirectHostname}/tracker/${this.tx.txid}`, - referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, - button: { shape: 'semiround', size: 'small', theme: 'light'} + redirectURL: `${redirectHostname}/tx/${this.tx.txid}`, + referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}` }); - if (this.step === 'cashapp') { - await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'light', size: 'small', shape: 'semiround' }) - } + await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'dark' }); this.loadingCashapp = false; 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$( @@ -590,9 +708,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy { tokenResult.token, tokenResult.details.cashAppPay.cashtag, tokenResult.details.cashAppPay.referenceId, - this.accelerationUUID + costUSD ).subscribe({ next: () => { + this.processing = false; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); if (this.cashAppPay) { @@ -607,6 +726,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(() => { @@ -626,7 +746,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { /** * BTCPay */ - async requestBTCPayInvoice() { + async requestBTCPayInvoice(): Promise { this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe( switchMap(response => { return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId); @@ -656,54 +776,61 @@ export class AccelerateCheckout implements OnInit, OnDestroy { /** * UI events */ - selectedOptionChanged(event) { + selectedOptionChanged(event): void { this.selectedOption = event.target.id; } - get step() { + get step(): CheckoutStep { return this._step; } - get paymentMethods() { - return Object.keys(this.estimate?.availablePaymentMethods || {}); + get paymentMethods(): PaymentMethod[] { + return Object.keys(this.estimate?.availablePaymentMethods || {}) as PaymentMethod[]; } - get couldPayWithBitcoin() { + get couldPayWithBitcoin(): boolean { return !!this.estimate?.availablePaymentMethods?.bitcoin; } - get couldPayWithCashapp() { + get couldPayWithCashapp(): boolean { if (!this.cashappEnabled) { return false; } return !!this.estimate?.availablePaymentMethods?.cashapp; } - get couldPayWithApplePay() { + get couldPayWithApplePay(): boolean { if (!this.applePayEnabled) { return false; } return !!this.estimate?.availablePaymentMethods?.applePay; } - get couldPayWithBalance() { + get couldPayWithGooglePay(): boolean { + if (!this.googlePayEnabled) { + return false; + } + return !!this.estimate?.availablePaymentMethods?.googlePay; + } + + get couldPayWithBalance(): boolean { if (!this.hasAccessToBalanceMode) { return false; } return !!this.estimate?.availablePaymentMethods?.balance; } - get couldPay() { - return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp || this.couldPayWithApplePay; + get couldPay(): boolean { + return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp || this.couldPayWithApplePay || this.couldPayWithGooglePay; } - get canPayWithBitcoin() { + get canPayWithBitcoin(): boolean { const paymentMethod = this.estimate?.availablePaymentMethods?.bitcoin; return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max; } - get canPayWithCashapp() { - if (!this.cashappEnabled || !this.conversions) { + get canPayWithCashapp(): boolean { + if (!this.cashappEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) { return false; } @@ -718,8 +845,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { return false; } - get canPayWithApplePay() { - if (!this.applePayEnabled || !this.conversions) { + get canPayWithApplePay(): boolean { + if (!this.applePayEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) { return false; } @@ -734,7 +861,23 @@ export class AccelerateCheckout implements OnInit, OnDestroy { return false; } - get canPayWithBalance() { + get canPayWithGooglePay(): boolean { + if (!this.googlePayEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) { + return false; + } + + const paymentMethod = this.estimate?.availablePaymentMethods?.googlePay; + if (paymentMethod) { + const costUSD = (this.cost / 100_000_000 * this.conversions.USD); + if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) { + return true; + } + } + + return false; + } + + get canPayWithBalance(): boolean { if (!this.hasAccessToBalanceMode) { return false; } @@ -742,11 +885,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy { return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max && this.cost <= this.estimate?.userBalance; } - get canPay() { - return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp || this.canPayWithApplePay; + get canPay(): boolean { + return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp || this.canPayWithApplePay || this.canPayWithGooglePay; } - get hasAccessToBalanceMode() { + get hasAccessToBalanceMode(): boolean { return this.isLoggedIn() && this.estimate?.hasAccess; } 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 new file mode 100644 index 000000000..0f436f9ac --- /dev/null +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.html @@ -0,0 +1,63 @@ +
+ + + + + + + + + + + + + @if (accelerationInfo.status === 'accelerated') { + + } @else { + + } + + + @if (accelerationInfo.status === 'seen') { + + + } @else if (accelerationInfo.status === 'accelerated' || accelerationInfo.status === 'mined') { + + @if (accelerationInfo.status === 'accelerated') { + + } @else { + + } + } + + + + + + +
Status + @if (accelerationInfo.status === 'seen') { + First seen + } @else if (accelerationInfo.status === 'accelerated') { + Accelerated + } @else if (accelerationInfo.status === 'mined') { + Mined + } +
Fee{{ accelerationInfo.fee | number }} sats
Out-of-band fees{{ accelerationInfo.feeDelta | number }} sats{{ accelerationInfo.bidBoost | number }} sats
Fee rateAccelerated fee rate
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 new file mode 100644 index 000000000..a8c4cd5cf --- /dev/null +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.scss @@ -0,0 +1,41 @@ +.acceleration-tooltip { + position: fixed; + z-index: 3; + background: color-mix(in srgb, var(--active-bg) 95%, transparent); + border-radius: 4px; + box-shadow: 1px 1px 10px rgba(0,0,0,0.5); + color: var(--tooltip-grey); + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 10px 15px; + text-align: left; + pointer-events: none; + + .badge.badge-accelerated { + background-color: var(--tertiary); + color: white; + } + + .value { + text-align: end; + } + + .label { + padding-right: 30px; + vertical-align: top; + } + + .pool-logo { + width: 22px; + height: 22px; + position: relative; + top: -1px; + margin-right: 4px; + margin-bottom: 4px; + } + + .oobFees { + color: #905cf4; + } +} diff --git a/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.ts b/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.ts new file mode 100644 index 000000000..b4b3405fc --- /dev/null +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.ts @@ -0,0 +1,38 @@ +import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core'; + +@Component({ + selector: 'app-acceleration-timeline-tooltip', + templateUrl: './acceleration-timeline-tooltip.component.html', + styleUrls: ['./acceleration-timeline-tooltip.component.scss'], +}) +export class AccelerationTimelineTooltipComponent implements OnChanges { + @Input() accelerationInfo: any; + @Input() cursorPosition: { x: number, y: number }; + + tooltipPosition: any = null; + + @ViewChild('tooltip') tooltipElement: ElementRef; + + constructor() {} + + ngOnChanges(changes): void { + if (changes.cursorPosition && changes.cursorPosition.currentValue) { + let x = Math.max(10, changes.cursorPosition.currentValue.x - 50); + let y = changes.cursorPosition.currentValue.y + 20; + if (this.tooltipElement) { + const elementBounds = this.tooltipElement.nativeElement.getBoundingClientRect(); + if ((x + elementBounds.width) > (window.innerWidth - 10)) { + x = Math.max(0, window.innerWidth - elementBounds.width - 10); + } + if (y + elementBounds.height > (window.innerHeight - 20)) { + y = y - elementBounds.height - 20; + } + } + this.tooltipPosition = { x, y }; + } + } + + hasPoolsData(): boolean { + return Object.keys(this.accelerationInfo.poolsData).length > 0; + } +} 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 28076efa5..af76bbc7b 100644 --- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html @@ -1,6 +1,6 @@
- @if (!tx.status.confirmed) { + @if (!tx.status.confirmed || canceled) {
@@ -8,8 +8,8 @@
- @if (eta) { - ~ + @if (eta && !canceled) { + ~ }
@@ -19,16 +19,20 @@
-
+
-
+
-
+
-
Mined
+ @if (canceled) { +
Canceled
+ } @else { +
Mined
+ }
@@ -38,18 +42,16 @@
- +
@if (tx.status.confirmed) { -
- -
- } @else if (standardETA && !tx.status.confirmed) { - + + } @else if (eta && canceled) { + ~ }
@@ -58,7 +60,7 @@
-
+
First seen
@@ -73,47 +75,50 @@
-
+
- @if (tx.status.confirmed) { + @if (tx.status.confirmed && !canceled) {
} @else {
} -
+
- @if (!tx.status.confirmed) { -
+ @if (!tx.status.confirmed || canceled) { +
}
- @if (tx.status.confirmed) { + @if (tx.status.confirmed && !canceled) {
Accelerated
} -
+
@if (!tx.status.confirmed) { Accelerated{{ "" }} } @if (useAbsoluteTime) { {{ acceleratedAt * 1000 | date }} } @else { - + }
- @if (tx.status.confirmed) { + @if (tx.status.confirmed && !canceled) {
} @else {
}
- @if (tx.status.confirmed) { + @if (tx.status.confirmed && !canceled) {
} @else {
} -
+
@if (tx.status.confirmed) { @@ -130,4 +135,10 @@
+ + +
diff --git a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.scss b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.scss index 93a0cdba1..2bd46199a 100644 --- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.scss +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.scss @@ -129,6 +129,9 @@ margin-left: calc(-4em + 5px); animation: goFasterLeft 0.8s infinite linear; } + &.no-animation { + animation: none; + } } &.left { @@ -152,9 +155,16 @@ margin-bottom: -8px; transform: translateY(-50%); border-radius: 50%; - cursor: pointer; padding: 4px; background: transparent; + transition: background-color 300ms, padding 300ms; + + &.hovering { + cursor: pointer; + &:hover { + padding: 0px; + } + } .shape { position: relative; 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 c8dbed72b..59e63d839 100644 --- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts @@ -1,6 +1,8 @@ -import { Component, Input, OnInit, OnChanges } from '@angular/core'; -import { ETA } from '../../services/eta.service'; -import { Transaction } from '../../interfaces/electrs.interface'; +import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core'; +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', @@ -9,47 +11,82 @@ import { Transaction } from '../../interfaces/electrs.interface'; }) 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; + @Input() canceled: boolean; - acceleratedAt: number; now: number; accelerateRatio: number; useAbsoluteTime: boolean = false; - interval: number; + firstSeenToAccelerated: number; + acceleratedToMined: number; - constructor() {} + tooltipPosition = null; + hoverInfo: any = null; + poolsData: { [id: number]: SinglePoolStats } = {}; + + constructor( + private miningService: MiningService, + ) {} 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.interval = window.setInterval(() => { - this.now = Math.floor(new Date().getTime() / 1000); - this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600; - }, 60000); + this.miningService.getPools().subscribe(pools => { + for (const pool of pools) { + this.poolsData[pool.unique_id] = pool; + } + }); } 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 { + if (status === 'seen') { + this.hoverInfo = { + status, + fee: this.tx.fee, + weight: this.tx.weight + }; + } else if (status === 'accelerated') { + this.hoverInfo = { + status, + fee: this.accelerationInfo?.effectiveFee || this.tx.fee, + weight: this.tx.weight, + feeDelta: this.accelerationInfo?.feeDelta || this.tx.feeDelta, + pools: this.tx.acceleratedBy || this.accelerationInfo?.pools, + poolsData: this.poolsData + }; + } else if (status === 'mined') { + this.hoverInfo = { + status, + fee: this.accelerationInfo?.effectiveFee, + weight: this.tx.weight, + bidBoost: this.accelerationInfo?.bidBoost, + minedByPoolUniqueId: this.accelerationInfo?.minedByPoolUniqueId, + pools: this.tx.acceleratedBy || this.accelerationInfo?.pools, + poolsData: this.poolsData + }; + } + } + + onBlur(event): void { + this.hoverInfo = null; + } + + @HostListener('pointermove', ['$event']) + onPointerMove(event) { + this.tooltipPosition = { x: event.clientX, y: event.clientY }; } } 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 b5e575409..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', @@ -23,7 +23,7 @@ import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; } `], changeDetection: ChangeDetectionStrategy.OnPush, @@ -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 12386e30c..225bf1955 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 @@ -4,8 +4,8 @@
-
- +
+
@@ -21,8 +21,8 @@ - - + + - - - - - - - - + @if (!pending) { + + + + + + + + + + + } @else { + + + + + + + + + }
TXID Requested
@@ -33,7 +33,7 @@ - {{ (acceleration.feeDelta) | number }} sat + {{ (acceleration.feeDelta) | number }} sats @@ -41,7 +41,7 @@ - {{ acceleration.boost | number }} sat + {{ acceleration.boost | number }} sats ~ @@ -62,8 +62,9 @@ Pending - Completed 🔄 - Failed 🔄 + Completed ⌛ + Mined ⌛ + Canceled ⌛ @@ -72,22 +73,47 @@
- - - - - - - -
+ + + + + + + + + + + +
+ + + + + + + +
diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.scss b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.scss index 56c92ff02..7f7f24bd5 100644 --- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.scss +++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.scss @@ -85,8 +85,8 @@ tr, td, th { } .pool-logo { - width: 22px; - height: 22px; + width: 18px; + height: 18px; position: relative; top: -1px; margin-right: 2px; 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 5b2b30d7a..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', @@ -32,6 +32,7 @@ export class AccelerationsListComponent implements OnInit, OnDestroy { dir: 'rtl' | 'ltr' = 'ltr'; paramSubscription: Subscription; pools: { [id: number]: SinglePoolStats } = {}; + nonEmptyAccelerations: boolean = true; constructor( private servicesApiService: ServicesApiServices, @@ -50,12 +51,21 @@ export class AccelerationsListComponent implements OnInit, OnDestroy { } ngOnInit(): void { + this.miningService.getPools().subscribe(pools => { + for (const pool of pools) { + this.pools[pool.unique_id] = pool; + } + }); + if (!this.widget) { 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); }) @@ -82,12 +92,6 @@ export class AccelerationsListComponent implements OnInit, OnDestroy { ).subscribe(() => { this.pageChange(this.page); }); - - this.miningService.getMiningStats('1m').subscribe(stats => { - for (const pool of stats.pools) { - this.pools[pool.poolUniqueId] = pool; - } - }); } this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()]; @@ -115,6 +119,7 @@ export class AccelerationsListComponent implements OnInit, OnDestroy { for (const acc of accelerations) { acc.boost = acc.boostCost != null ? acc.boostCost : acc.bidBoost; } + this.nonEmptyAccelerations = accelerations.length > 0; if (this.widget) { return of(accelerations.slice(0, 6)); } else { @@ -146,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 83ecad459..be5d7e021 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 @@ -1,7 +1,7 @@ @if (chartOnly) { } @else { - +
@@ -10,17 +10,17 @@ diff --git a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.scss b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.scss index b01a902a4..041a66ea2 100644 --- a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.scss +++ b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.scss @@ -61,4 +61,8 @@ & > div, & > div > svg { overflow: visible !important; } +} + +.oobFees { + color: #905cf4; } \ No newline at end of file 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 46ba12816..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); } @@ -67,16 +70,27 @@ export class ActiveAccelerationBox implements OnChanges { const acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate); const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0); + // Find the first pool with at least 1% of the total network hashrate + const firstSignificantPool = acceleratingPools.findIndex(pool => pools[pool].lastEstimatedHashrate > this.miningStats.lastEstimatedHashrate / 100); + const numSignificantPools = acceleratingPools.length - firstSignificantPool; acceleratingPools.forEach((poolId, index) => { const pool = pools[poolId]; const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1); + 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, - toRGB(lighten({ r: 147, g: 57, b: 244 }, index * .08)), + 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( @@ -127,6 +141,7 @@ export class ActiveAccelerationBox implements OnChanges { } ] }; + this.cd.markForCheck(); } onChartInit(ec) { @@ -139,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/acceleration/sparkles/acceleration-sparkles.component.html b/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.html new file mode 100644 index 000000000..bf0080344 --- /dev/null +++ b/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.html @@ -0,0 +1,5 @@ +
+
+ + +
+
\ No newline at end of file diff --git a/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.scss b/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.scss new file mode 100644 index 000000000..35f6e32d5 --- /dev/null +++ b/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.scss @@ -0,0 +1,45 @@ +.sparkles { + position: absolute; + top: var(--block-size); + height: 50px; + right: 0; +} + +.sparkle { + position: absolute; + color: rgba(152, 88, 255, 0.75); + opacity: 0; + transform: scale(0.8) rotate(0deg); + animation: pop ease 2000ms forwards, sparkle ease 500ms infinite; +} + +.inner-sparkle { + display: block; +} + +@keyframes pop { + 0% { + transform: scale(0.8) rotate(0deg); + opacity: 0; + } + 20% { + transform: scale(1) rotate(72deg); + opacity: 1; + } + 100% { + transform: scale(0) rotate(360deg); + opacity: 0; + } +} + +@keyframes sparkle { + 0% { + color: rgba(152, 88, 255, 0.75); + } + 50% { + color: rgba(198, 162, 255, 0.75); + } + 100% { + color: rgba(152, 88, 255, 0.75); + } +} \ No newline at end of file diff --git a/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.ts b/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.ts new file mode 100644 index 000000000..2316c996d --- /dev/null +++ b/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.ts @@ -0,0 +1,73 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; + +@Component({ + selector: 'app-acceleration-sparkles', + templateUrl: './acceleration-sparkles.component.html', + styleUrls: ['./acceleration-sparkles.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AccelerationSparklesComponent implements OnChanges { + @Input() arrow: ElementRef; + @Input() run: boolean = false; + + @ViewChild('sparkleAnchor') + sparkleAnchor: ElementRef; + + constructor( + private cd: ChangeDetectorRef, + ) {} + + endTimeout: any; + lastSparkle: number = 0; + sparkleWidth: number = 0; + sparkles: any[] = []; + + ngOnChanges(changes: SimpleChanges): void { + if (changes.run) { + if (this.endTimeout) { + clearTimeout(this.endTimeout); + this.endTimeout = null; + } + if (this.run) { + this.doSparkle(); + } else { + this.endTimeout = setTimeout(() => { + this.sparkles = []; + }, 2000); + } + } + } + + doSparkle(): void { + if (this.run) { + const now = performance.now(); + if (now - this.lastSparkle > 20) { + this.lastSparkle = now; + if (this.arrow?.nativeElement && this.sparkleAnchor?.nativeElement) { + const anchor = this.sparkleAnchor.nativeElement.getBoundingClientRect().right; + const right = this.arrow.nativeElement.getBoundingClientRect().right; + const dx = (anchor - right) + 30; + const numSparkles = Math.ceil(Math.random() * 3); + for (let i = 0; i < numSparkles; i++) { + this.sparkles.push({ + style: { + right: (dx + (Math.random() * 10)) + 'px', + top: (15 + (Math.random() * 30)) + 'px', + }, + rotation: { + transform: `rotate(${Math.random() * 360}deg)`, + } + }); + } + while (this.sparkles.length > 200) { + this.sparkles.shift(); + } + this.cd.markForCheck(); + } + } + requestAnimationFrame(() => { + this.doSparkle(); + }); + } + } +} \ No newline at end of file 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 388c5eb00..1b320a38a 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,15 @@ 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'; const periodSeconds = { '1d': (60 * 60 * 24), @@ -30,7 +29,7 @@ const periodSeconds = { position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; } `], changeDetection: ChangeDetectionStrategy.OnPush, @@ -45,6 +44,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { @Input() right: number | string = 10; @Input() left: number | string = 70; @Input() widget: boolean = false; + @Input() defaultFiat: boolean = false; data: any[] = []; fiatData: any[] = []; @@ -77,7 +77,6 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { private relativeUrlPipe: RelativeUrlPipe, private priceService: PriceService, private fiatCurrencyPipe: FiatCurrencyPipe, - private fiatShortenerPipe: FiatShortenerPipe, private zone: NgZone, ) {} @@ -86,6 +85,9 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { if (!this.addressSummary$ && (!this.address || !this.stats)) { return; } + if (changes.defaultFiat) { + this.selected['Fiat'] = !!this.defaultFiat; + } if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) { if (this.subscription) { this.subscription.unsubscribe(); @@ -147,7 +149,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { if (!summary) { return; } - + 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 => { @@ -161,7 +163,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { d }; }).reverse(); - + this.data = processData.filter(({ d }) => d.txid !== undefined).map(({ time, balance, d }) => [time, balance, d]); this.fiatData = processData.map(({ time, fiatBalance, balance, d }) => [time, fiatBalance, d, balance]); @@ -179,6 +181,9 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0); const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] ?? d.value[1])), maxValue); + this.right = this.selected['Fiat'] ? +this.initialRight + 40 : this.initialRight; + this.left = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? this.initialLeft : +this.initialLeft - 40; + this.chartOptions = { color: [ new echarts.graphic.LinearGradient(0, 0, 0, 1, [ @@ -245,21 +250,22 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { let tooltip = '
'; const hasTx = data[0].data[2].txid; + const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' }); + + tooltip += `
+
+
${date}
`; + if (hasTx) { const header = data.length === 1 ? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}` : `${data.length} transactions`; - tooltip += `${header}`; + tooltip += `
${header}
`; } - - const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' }); - - tooltip += `
-
`; - + const formatBTC = (val, decimal) => (val / 100_000_000).toFixed(decimal); const formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD'); - + const btcVal = btcData.reduce((total, d) => total + d.data[2].value, 0); const fiatVal = fiatData.reduce((total, d) => total + d.data[2].value * d.data[2].price / 100_000_000, 0); const btcColor = btcVal === 0 ? '' : (btcVal > 0 ? 'var(--green)' : 'var(--red)'); @@ -291,7 +297,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { } } - tooltip += `
${date}
`; + tooltip += `
`; return tooltip; }.bind(this) }, @@ -311,18 +317,21 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { formatter: (val): string => { let valSpan = maxValue - (this.period === 'all' ? 0 : minValue); if (valSpan > 100_000_000_000) { - return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0)} BTC`; + return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0, undefined, true)} BTC`; } else if (valSpan > 1_000_000_000) { - return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2)} BTC`; + return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2, undefined, true)} BTC`; } else if (valSpan > 100_000_000) { return `${(val / 100_000_000).toFixed(1)} BTC`; } else if (valSpan > 10_000_000) { return `${(val / 100_000_000).toFixed(2)} BTC`; } else if (valSpan > 1_000_000) { + if (maxValue > 100_000_000_000) { + return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 3, undefined, true)} BTC`; + } return `${(val / 100_000_000).toFixed(3)} BTC`; } else { - return `${this.amountShortenerPipe.transform(val, 0)} sats`; + return `${this.amountShortenerPipe.transform(val, 0, undefined, true)} sats`; } } }, @@ -336,7 +345,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { axisLabel: { color: 'rgb(110, 112, 121)', formatter: function(val) { - return this.fiatShortenerPipe.transform(val, null, 'USD'); + return `$${this.amountShortenerPipe.transform(val, 0, undefined, true)}`; }.bind(this) }, splitLine: { @@ -440,7 +449,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { right: this.right, }] : undefined }; - + if (this.chartInstance) { this.chartInstance.setOption(this.chartOptions); } 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 dd81b9809..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', @@ -55,7 +55,7 @@ export class AddressLabelsComponent implements OnChanges { } handleVin() { - const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin]) + const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin]); if (address?.scripts.size) { const script = address?.scripts.values().next().value; if (script.template?.label) { 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 83424791b..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', 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 ` +
Accelerated to
- @if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) { - + @if (accelerationInfo?.acceleratedFeeRate && (!effectiveFeeRate || accelerationInfo.acceleratedFeeRate >= effectiveFeeRate)) { + } @else { - + }
@if (hasCpfp) { - + }
@@ -36,7 +36,7 @@
- +
+ + + + + + + + + + + + + + + + + + + + + +
${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.ts b/frontend/src/app/components/balance-widget/balance-widget.component.ts index f830587cc..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({ 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 ba3489e17..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({ @@ -23,7 +23,7 @@ import { ActivatedRoute, Router } from '@angular/router'; position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; } `], changeDetection: ChangeDetectionStrategy.OnPush, 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 eb567c2a6..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', @@ -23,7 +23,7 @@ import { StateService } from '../../services/state.service'; position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; } `], changeDetection: ChangeDetectionStrategy.OnPush, 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 18e57ada8..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', @@ -24,7 +24,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; } `], changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/frontend/src/app/components/block-filters/block-filters.component.html b/frontend/src/app/components/block-filters/block-filters.component.html index 2a22681d6..be692cfa9 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.html +++ b/frontend/src/app/components/block-filters/block-filters.component.html @@ -1,6 +1,5 @@
- - beta +
diff --git a/frontend/src/app/components/block-filters/block-filters.component.scss b/frontend/src/app/components/block-filters/block-filters.component.scss index 7cbc738b7..4312dd7b2 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.scss +++ b/frontend/src/app/components/block-filters/block-filters.component.scss @@ -24,7 +24,6 @@ display: flex; flex-direction: row; align-items: center; - float: right; &:hover, &:active { text-decoration: none; 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 59f34cd45..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', @@ -21,7 +21,7 @@ import { StateService } from '../../services/state.service'; position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; } `], changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html index 2ef07d12c..471257230 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html @@ -2,10 +2,12 @@
-
-
-
not available
-
+ @if (!disableSpinner) { +
+
+
not available
+
+ } flagSet || tx.flags > 0, false); if (filtersAvailable !== this.filtersAvailable) { this.setFilterFlags(); @@ -204,7 +206,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On this.filtersAvailable = filtersAvailable; if (this.scene) { this.clearUpdateQueue(); - this.scene.setup(transactions); + this.scene.setup(transactions, sort); this.readyNextFrame = true; this.start(); this.updateSearchHighlight(); 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 c59fcb7d4..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}}; @@ -88,16 +88,19 @@ export default class BlockScene { } // set up the scene with an initial set of transactions, without any transition animation - setup(txs: TransactionStripped[]) { + setup(txs: TransactionStripped[], sort: boolean = false) { // clean up any old transactions Object.values(this.txs).forEach(tx => { tx.destroy(); delete this.txs[tx.txid]; }); this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight }); - txs.forEach(tx => { - const txView = new TxView(tx, this); - this.txs[tx.txid] = txView; + let txViews = txs.map(tx => new TxView(tx, this)); + if (sort) { + txViews = txViews.sort(feeRateDescending); + } + txViews.forEach(txView => { + this.txs[txView.txid] = txView; this.place(txView); this.saveGridToScreenPosition(txView); this.applyTxUpdate(txView, { @@ -914,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 742c305f5..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'); @@ -33,7 +33,7 @@ export default class TxView implements TransactionStripped { flags: number; bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n; time?: number; - status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated'; + status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'added_deprioritized' | 'deprioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated'; context?: 'projected' | 'actual'; scene?: BlockScene; diff --git a/frontend/src/app/components/block-overview-graph/utils.ts b/frontend/src/app/components/block-overview-graph/utils.ts index 9a6d9da43..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, @@ -71,6 +84,7 @@ export const defaultAuditColors = { censored: hexToColor('f344df'), missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), added: hexToColor('0099ff'), + added_prioritized: darken(desaturate(hexToColor('0099ff'), 0.15), 0.85), prioritized: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7), accelerated: hexToColor('8f5ff6'), }; @@ -101,6 +115,7 @@ export const contrastAuditColors = { censored: hexToColor('ffa8ff'), missing: darken(desaturate(hexToColor('ffa8ff'), 0.3), 0.7), added: hexToColor('00bb98'), + added_prioritized: darken(desaturate(hexToColor('00bb98'), 0.15), 0.85), prioritized: darken(desaturate(hexToColor('00bb98'), 0.3), 0.7), accelerated: hexToColor('8f5ff6'), }; @@ -136,8 +151,14 @@ export function defaultColorFunction( return auditColors.missing; case 'added': return auditColors.added; + case 'added_prioritized': + return auditColors.added_prioritized; case 'prioritized': return auditColors.prioritized; + case 'added_deprioritized': + return auditColors.added_prioritized; + case 'deprioritized': + return auditColors.prioritized; case 'selected': return colors.marginal[levelIndex] || colors.marginal[defaultMempoolFeeColors.length - 1]; case 'accelerated': 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 bfb4cd206..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 @@ -51,7 +51,7 @@ Effective fee rate Accelerated fee rate - + @@ -75,6 +75,15 @@ Recently CPFP'd Added Prioritized + + Added + Prioritized + + Deprioritized + + Added + Deprioritized + Marginal fee rate Conflict Accelerated diff --git a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.scss b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.scss index 28708506b..0b2c05948 100644 --- a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.scss +++ b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.scss @@ -27,6 +27,9 @@ th, td { width: 70%; text-align: end; } + &.oobFees { + color: #905cf4; + } } .badge.badge-accelerated { 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 d0c154420..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', @@ -23,7 +23,7 @@ import { StateService } from '../../services/state.service'; position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; } `], changeDetection: ChangeDetectionStrategy.OnPush, 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 bf591ad70..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', @@ -21,7 +21,7 @@ import { StateService } from '../../services/state.service'; position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; } `], changeDetection: ChangeDetectionStrategy.OnPush, 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 01702487f..dab3c00fa 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -1,22 +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 { 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', @@ -318,7 +319,7 @@ export class BlockComponent implements OnInit, OnDestroy { this.accelerationsSubscription = this.block$.pipe( switchMap((block) => { return this.stateService.env.ACCELERATOR === true && block.height > 819500 - ? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height }) + ? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height }) .pipe(catchError(() => { return of([]); })) @@ -326,7 +327,7 @@ export class BlockComponent implements OnInit, OnDestroy { }) ).subscribe((accelerations) => { this.accelerations = accelerations; - if (accelerations.length) { + if (accelerations.length && this.strippedTransactions) { // Don't call setupBlockAudit if we don't have transactions yet; it will be called later in overviewSubscription this.setupBlockAudit(); } }); @@ -521,8 +522,10 @@ export class BlockComponent implements OnInit, OnDestroy { if (transactions && blockAudit) { const inTemplate = {}; const inBlock = {}; + const isUnseen = {}; const isAdded = {}; const isPrioritized = {}; + const isDeprioritized = {}; const isCensored = {}; const isMissing = {}; const isSelected = {}; @@ -534,6 +537,17 @@ export class BlockComponent implements OnInit, OnDestroy { this.numUnexpected = 0; if (blockAudit?.template) { + // augment with locally calculated *de*prioritized transactions if possible + const { prioritized, deprioritized } = identifyPrioritizedTransactions(transactions); + // but if the local calculation produces returns unexpected results, don't use it + let useLocalDeprioritized = deprioritized.length < (transactions.length * 0.1); + for (const tx of prioritized) { + if (!isPrioritized[tx] && !isAccelerated[tx]) { + useLocalDeprioritized = false; + break; + } + } + for (const tx of blockAudit.template) { inTemplate[tx.txid] = true; if (tx.acc) { @@ -543,12 +557,20 @@ export class BlockComponent implements OnInit, OnDestroy { for (const tx of transactions) { inBlock[tx.txid] = true; } + for (const txid of blockAudit.unseenTxs || []) { + isUnseen[txid] = true; + } for (const txid of blockAudit.addedTxs) { isAdded[txid] = true; } - for (const txid of blockAudit.prioritizedTxs || []) { + for (const txid of blockAudit.prioritizedTxs) { isPrioritized[txid] = true; } + if (useLocalDeprioritized) { + for (const txid of deprioritized || []) { + isDeprioritized[txid] = true; + } + } for (const txid of blockAudit.missingTxs) { isCensored[txid] = true; } @@ -592,18 +614,33 @@ export class BlockComponent implements OnInit, OnDestroy { tx.status = 'accelerated'; } } - for (const [index, tx] of transactions.entries()) { + let anySeen = false; + for (let index = transactions.length - 1; index >= 0; index--) { + const tx = transactions[index]; tx.context = 'actual'; if (index === 0) { tx.status = null; - } else if (isAdded[tx.txid]) { - tx.status = 'added'; } else if (isPrioritized[tx.txid]) { - tx.status = 'prioritized'; + if (isAdded[tx.txid] || (blockAudit.version > 0 && isUnseen[tx.txid])) { + tx.status = 'added_prioritized'; + } else { + tx.status = 'prioritized'; + } + } else if (isDeprioritized[tx.txid]) { + if (isAdded[tx.txid] || (blockAudit.version > 0 && isUnseen[tx.txid])) { + tx.status = 'added_deprioritized'; + } else { + tx.status = 'deprioritized'; + } + } else if (isAdded[tx.txid] && (blockAudit.version === 0 || isUnseen[tx.txid])) { + tx.status = 'added'; } else if (inTemplate[tx.txid]) { + anySeen = true; tx.status = 'found'; } else if (isRbf[tx.txid]) { tx.status = 'rbf'; + } else if (isUnseen[tx.txid] && anySeen) { + tx.status = 'added'; } else { tx.status = 'selected'; isSelected[tx.txid] = true; @@ -785,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..9c04058e9 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,14 @@ 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'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; @Component({ selector: 'app-blocks-list', @@ -49,6 +50,7 @@ export class BlocksList implements OnInit { private ogService: OpenGraphService, private route: ActivatedRoute, private router: Router, + private relativeUrlPipe: RelativeUrlPipe, @Inject(LOCALE_ID) private locale: string, ) { this.isMempoolModule = this.stateService.env.BASE_MODULE === 'mempool'; @@ -182,7 +184,7 @@ export class BlocksList implements OnInit { } pageChange(page: number): void { - this.router.navigate(['blocks', page]); + this.router.navigate([this.relativeUrlPipe.transform('/blocks/'), page]); } trackByBlock(index: number, block: BlockExtended): number { 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/clipboard/clipboard.component.html b/frontend/src/app/components/clipboard/clipboard.component.html index d23ccdf8c..c3a18d90b 100644 --- a/frontend/src/app/components/clipboard/clipboard.component.html +++ b/frontend/src/app/components/clipboard/clipboard.component.html @@ -1,15 +1,17 @@ - - - + {{ copiedMessage }} diff --git a/frontend/src/app/components/clipboard/clipboard.component.scss b/frontend/src/app/components/clipboard/clipboard.component.scss index 49294e548..6ae620ae7 100644 --- a/frontend/src/app/components/clipboard/clipboard.component.scss +++ b/frontend/src/app/components/clipboard/clipboard.component.scss @@ -7,7 +7,19 @@ padding-left: 0.4rem; } -img { - position: relative; - left: -3px; -} \ No newline at end of file +.copied-message { + background: color-mix(in srgb, var(--active-bg) 95%, transparent); + color: var(--fg); + font-family: sans-serif; + font-size: .8rem; + font-weight: 400; + text-decoration: none; + text-align: left; + padding: .6em .75rem; + border-radius: 4px; + position: absolute; + white-space: nowrap; + box-shadow: 0 .5rem 1rem -.5rem #000; + z-index: 1000; + opacity: .9; +} diff --git a/frontend/src/app/components/clipboard/clipboard.component.ts b/frontend/src/app/components/clipboard/clipboard.component.ts index 6e577d8b3..31f882d12 100644 --- a/frontend/src/app/components/clipboard/clipboard.component.ts +++ b/frontend/src/app/components/clipboard/clipboard.component.ts @@ -1,6 +1,4 @@ -import { Component, ViewChild, ElementRef, AfterViewInit, Input, ChangeDetectionStrategy } from '@angular/core'; -import * as ClipboardJS from 'clipboard'; -import * as tlite from 'tlite'; +import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; @Component({ selector: 'app-clipboard', @@ -8,15 +6,14 @@ import * as tlite from 'tlite'; styleUrls: ['./clipboard.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ClipboardComponent implements AfterViewInit { - @ViewChild('btn') btn: ElementRef; - @ViewChild('buttonWrapper') buttonWrapper: ElementRef; +export class ClipboardComponent { @Input() button = false; @Input() class = 'btn btn-secondary ml-1'; @Input() size: 'small' | 'normal' | 'large' = 'normal'; @Input() text: string; @Input() leftPadding = true; copiedMessage: string = $localize`:@@clipboard.copied-message:Copied!`; + showMessage = false; widths = { small: '10', @@ -24,22 +21,40 @@ export class ClipboardComponent implements AfterViewInit { large: '18', }; - clipboard: any; + constructor( + private cd: ChangeDetectorRef, + ) { } - constructor() { } - - ngAfterViewInit() { - this.clipboard = new ClipboardJS(this.btn.nativeElement); - this.clipboard.on('success', () => { - tlite.show(this.buttonWrapper.nativeElement); - setTimeout(() => { - tlite.hide(this.buttonWrapper.nativeElement); - }, 1000); - }); + async copyText() { + if (this.text && !this.showMessage) { + try { + await this.copyToClipboard(this.text); + this.showMessage = true; + this.cd.markForCheck(); + setTimeout(() => { + this.showMessage = false; + this.cd.markForCheck(); + }, 1000); + } catch (error) { + console.error('Clipboard copy failed:', error); + } + } } - onDestroy() { - this.clipboard.destroy(); + async copyToClipboard(text: string) { + if (navigator.clipboard) { + await navigator.clipboard.writeText(text); + } else { + // Use the 'out of viewport hidden text area' trick on non-secure contexts + const textarea = document.createElement('textarea'); + textarea.value = this.text; + textarea.style.opacity = '0'; + textarea.setAttribute('readonly', 'true'); // Don't trigger keyboard on mobile + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + textarea.remove(); + } } } 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 65f0dc0ab..b6e374587 100644 --- a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html +++ b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html @@ -238,7 +238,7 @@   - +
@@ -267,9 +267,11 @@
@@ -313,4 +315,4 @@ - \ No newline at end of file + 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 622e6cf3a..36af77d6d 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; @@ -73,7 +73,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni { index: 0, name: $localize`:@@dfc3c34e182ea73c5d784ff7c8135f087992dac1:All`, mode: 'and', filters: [], gradient: 'age' }, { index: 1, name: $localize`Consolidation`, mode: 'and', filters: ['consolidation'], gradient: 'fee' }, { index: 2, name: $localize`Coinjoin`, mode: 'and', filters: ['coinjoin'], gradient: 'fee' }, - { index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'], gradient: 'fee' }, + { index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'fake_scripthash', 'op_return'], gradient: 'fee' }, ]; goggleFlags = 0n; goggleMode: FilterMode = 'and'; @@ -370,23 +370,47 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni 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( + this.walletSummary$ = this.apiService.getWallet$(walletName).pipe( catchError(e => { - return of(null); + return of({}); }), - map((walletTransactions) => { - const transactions = Object.values(walletTransactions).flatMap(wallet => wallet.transactions); - return this.deduplicateWalletTransactions(transactions); - }), - switchMap(initial => this.stateService.walletTransactions$.pipe( - startWith(null), - scan((summary, walletTransactions) => { - if (walletTransactions) { - const transactions: AddressTxSummary[] = [...summary, ...Object.values(walletTransactions).flat()]; - return this.deduplicateWalletTransactions(transactions); + 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 summary; - }, initial) + return this.deduplicateWalletTransactions([...summaries, ...newSummaries]); + }, this.deduplicateWalletTransactions(Object.values(wallet).flatMap(address => address.transactions))) )), share(), ); 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 7db1367ea..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', @@ -15,7 +15,7 @@ import { StateService } from '../../services/state.service'; position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; } `], }) 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 89e6bb8a8..3165ae9a7 100644 --- a/frontend/src/app/components/faucet/faucet.component.html +++ b/frontend/src/app/components/faucet/faucet.component.html @@ -5,7 +5,7 @@
- + @if (txid) {
@@ -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') {
@@ -36,6 +44,13 @@
} + @else if (error === 'account_limited') { +
+
+ Your Twitter account does not allow you to access the faucet +
+
+ } @else if (error) { @@ -81,7 +96,7 @@ } - @if (status?.address) { + @if (status?.address) {
If you no longer need your testnet4 coins, please consider sending them back to replenish the faucet.
} diff --git a/frontend/src/app/components/faucet/faucet.component.ts b/frontend/src/app/components/faucet/faucet.component.ts index 891b6310d..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', @@ -19,7 +18,7 @@ export class FaucetComponent implements OnInit, OnDestroy { error: string = ''; user: any = undefined; txid: string = ''; - + faucetStatusSubscription: Subscription; status: { min: number; // minimum amount to request at once (in sats) @@ -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 8aaa983fa..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', @@ -23,7 +23,7 @@ import { seoDescriptionNetwork } from '../../shared/common.utils'; position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; } `], changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss index fe38ce89b..87ee1b45d 100644 --- a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss +++ b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss @@ -59,7 +59,7 @@ position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; } .loadingGraphs.widget { top: 75%; 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 5ac2cc10d..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; @@ -28,7 +28,7 @@ interface Hashrate { position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; } `], changeDetection: ChangeDetectionStrategy.OnPush, 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 d3d7d8237..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; @@ -17,7 +17,7 @@ const OUTLIERS_MEDIAN_MULTIPLIER = 4; position: absolute; top: 50%; left: calc(50% - 16px); - z-index: 100; + z-index: 99; } `], changeDetection: ChangeDetectionStrategy.OnPush, 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 aca469ff8..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', @@ -11,7 +11,7 @@ import { StateService } from '../../services/state.service'; position: absolute; top: 50%; left: calc(50% - 16px); - z-index: 100; + z-index: 99; } `], templateUrl: './lbtc-pegs-graph.component.html', diff --git a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html index b2dd0344b..7e39d9341 100644 --- a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html +++ b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html @@ -52,7 +52,7 @@ Mainnet Signet Testnet3 - Testnet4 beta + Testnet4 Liquid Liquid Testnet 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.scss b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.scss index 9881148fc..b58e706e5 100644 --- a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.scss +++ b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.scss @@ -2,5 +2,5 @@ position: absolute; top: 50%; left: calc(50% - 16px); - z-index: 100; + z-index: 99; } \ No newline at end of file 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.scss b/frontend/src/app/components/loading-indicator/loading-indicator.component.scss index b919fa2b8..af84083b7 100644 --- a/frontend/src/app/components/loading-indicator/loading-indicator.component.scss +++ b/frontend/src/app/components/loading-indicator/loading-indicator.component.scss @@ -1,7 +1,7 @@ .sticky-loading { position: absolute; right: 10px; - z-index: 100; + z-index: 1000; font-size: 14px; @media (width >= 992px) { left: 32px; 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.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 ea20cdf95..557529eef 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -70,7 +70,7 @@ Mainnet Signet Testnet3 - Testnet4 beta + Testnet4 Liquid Liquid Testnet
@@ -85,7 +85,6 @@
-
+ +
diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts index 13608bb73..3e429fa9f 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -1,17 +1,17 @@ -import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core'; +import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core'; import { Subscription, Observable, of, combineLatest } from 'rxjs'; -import { MempoolBlock } from '../../interfaces/websocket.interface'; -import { StateService } from '../../services/state.service'; -import { EtaService } from '../../services/eta.service'; +import { MempoolBlock } from '@interfaces/websocket.interface'; +import { StateService } from '@app/services/state.service'; +import { EtaService } from '@app/services/eta.service'; import { Router } from '@angular/router'; import { delay, filter, map, switchMap, tap } from 'rxjs/operators'; -import { feeLevels } from '../../app.constants'; -import { specialBlocks } from '../../app.constants'; -import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; +import { feeLevels } from '@app/app.constants'; +import { specialBlocks } from '@app/app.constants'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; import { Location } from '@angular/common'; -import { DifficultyAdjustment, MempoolPosition } from '../../interfaces/node-api.interface'; +import { DifficultyAdjustment, MempoolPosition } from '@interfaces/node-api.interface'; import { animate, style, transition, trigger } from '@angular/animations'; -import { ThemeService } from '../../services/theme.service'; +import { ThemeService } from '@app/services/theme.service'; @Component({ selector: 'app-mempool-blocks', @@ -77,6 +77,9 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { maxArrowPosition = 0; rightPosition = 0; transition = 'background 2s, right 2s, transform 1s'; + @ViewChild('arrowUp') + arrowElement: ElementRef; + acceleratingArrow: boolean = false; markIndex: number; txPosition: MempoolPosition; @@ -201,6 +204,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { this.markBlocksSubscription = this.stateService.markBlock$ .subscribe((state) => { + const oldTxPosition = this.txPosition; this.markIndex = undefined; this.txPosition = undefined; this.txFeePerVSize = undefined; @@ -209,6 +213,12 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { } if (state.mempoolPosition) { this.txPosition = state.mempoolPosition; + if (this.txPosition.accelerated && !oldTxPosition?.accelerated) { + this.acceleratingArrow = true; + setTimeout(() => { + this.acceleratingArrow = false; + }, 2000); + } } if (state.txFeePerVSize) { this.txFeePerVSize = state.txFeePerVSize; @@ -257,7 +267,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { if (event.key === prevKey) { if (this.mempoolBlocks[this.markIndex - 1]) { - this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]); + this.router.navigate([this.relativeUrlPipe.transform('/mempool-block/'), this.markIndex - 1]); } else { const blocks = this.stateService.blocksSubject$.getValue(); for (const block of (blocks || [])) { @@ -462,4 +472,4 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { } return emptyBlocks; } -} \ No newline at end of file +} diff --git a/frontend/src/app/components/mempool-graph/mempool-graph.component.ts b/frontend/src/app/components/mempool-graph/mempool-graph.component.ts index 63fb52a0c..15e6c6f7a 100644 --- a/frontend/src/app/components/mempool-graph/mempool-graph.component.ts +++ b/frontend/src/app/components/mempool-graph/mempool-graph.component.ts @@ -1,14 +1,14 @@ import { Component, OnInit, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, OnChanges } from '@angular/core'; -import { VbytesPipe } from '../../shared/pipes/bytes-pipe/vbytes.pipe'; -import { WuBytesPipe } from '../../shared/pipes/bytes-pipe/wubytes.pipe'; -import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; +import { VbytesPipe } from '@app/shared/pipes/bytes-pipe/vbytes.pipe'; +import { WuBytesPipe } from '@app/shared/pipes/bytes-pipe/wubytes.pipe'; +import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe'; import { formatNumber } from '@angular/common'; -import { OptimizedMempoolStats } from '../../interfaces/node-api.interface'; -import { StateService } from '../../services/state.service'; -import { StorageService } from '../../services/storage.service'; -import { EChartsOption } from '../../graphs/echarts'; -import { feeLevels, chartColors } from '../../app.constants'; -import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/graphs.utils'; +import { OptimizedMempoolStats } from '@interfaces/node-api.interface'; +import { StateService } from '@app/services/state.service'; +import { StorageService } from '@app/services/storage.service'; +import { EChartsOption } from '@app/graphs/echarts'; +import { feeLevels, chartColors } from '@app/app.constants'; +import { download, formatterXAxis, formatterXAxisLabel } from '@app/shared/graphs.utils'; @Component({ selector: 'app-mempool-graph', @@ -18,7 +18,7 @@ import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/grap position: absolute; top: 50%; left: calc(50% - 16px); - z-index: 100; + z-index: 99; } `], changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/frontend/src/app/components/menu/menu.component.html b/frontend/src/app/components/menu/menu.component.html index 23605ce55..848f505a1 100644 --- a/frontend/src/app/components/menu/menu.component.html +++ b/frontend/src/app/components/menu/menu.component.html @@ -12,9 +12,15 @@ OG #{{ user.ogRank }} - - {{ user.subscription_tag.toUpperCase() }} - + @if (user.subscription_tag !== 'free') { + + {{ user.subscription_tag.toUpperCase() }} + + } @else if (user.type === 'mining_pool') { + + MINING POOL + + } diff --git a/frontend/src/app/components/menu/menu.component.ts b/frontend/src/app/components/menu/menu.component.ts index 719495bb0..278ec46a1 100644 --- a/frontend/src/app/components/menu/menu.component.ts +++ b/frontend/src/app/components/menu/menu.component.ts @@ -1,11 +1,11 @@ import { Component, OnInit, Input, Output, EventEmitter, HostListener, OnDestroy } from '@angular/core'; import { Observable } from 'rxjs'; -import { MenuGroup } from '../../interfaces/services.interface'; -import { StorageService } from '../../services/storage.service'; +import { MenuGroup } from '@interfaces/services.interface'; +import { StorageService } from '@app/services/storage.service'; import { Router, NavigationStart } from '@angular/router'; -import { StateService } from '../../services/state.service'; -import { IUser, ServicesApiServices } from '../../services/services-api.service'; -import { AuthServiceMempool } from '../../services/auth.service'; +import { StateService } from '@app/services/state.service'; +import { IUser, ServicesApiServices } from '@app/services/services-api.service'; +import { AuthServiceMempool } from '@app/services/auth.service'; @Component({ selector: 'app-menu', diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts index 0e0974808..464866c40 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts @@ -1,8 +1,8 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, OnInit } from '@angular/core'; -import { SeoService } from '../../services/seo.service'; -import { OpenGraphService } from '../../services/opengraph.service'; -import { WebsocketService } from '../../services/websocket.service'; -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 { StateService } from '@app/services/state.service'; import { EventType, NavigationStart, Router } from '@angular/router'; @Component({ diff --git a/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.ts b/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.ts index 8c5dcbfcb..bc835b4d2 100644 --- a/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.ts +++ b/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.ts @@ -24,8 +24,8 @@ import { } from '@angular/forms'; import { takeUntil } from 'rxjs/operators'; -import { MultiSelectSearchFilter } from './search-filter.pipe'; -import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts, } from './types'; +import { MultiSelectSearchFilter } from '@components/ngx-bootstrap-multiselect/search-filter.pipe'; +import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts, } from '@components/ngx-bootstrap-multiselect/types'; import { Subject, Observable } from 'rxjs'; const MULTISELECT_VALUE_ACCESSOR: any = { diff --git a/frontend/src/app/components/ngx-bootstrap-multiselect/search-filter.pipe.ts b/frontend/src/app/components/ngx-bootstrap-multiselect/search-filter.pipe.ts index 1dfb57ffd..8c9232501 100644 --- a/frontend/src/app/components/ngx-bootstrap-multiselect/search-filter.pipe.ts +++ b/frontend/src/app/components/ngx-bootstrap-multiselect/search-filter.pipe.ts @@ -1,5 +1,5 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { IMultiSelectOption } from './types'; +import { IMultiSelectOption } from '@components/ngx-bootstrap-multiselect/types'; interface StringHashMap { [k: string]: T; diff --git a/frontend/src/app/components/ord-data/ord-data.component.html b/frontend/src/app/components/ord-data/ord-data.component.html new file mode 100644 index 000000000..8e58e66a4 --- /dev/null +++ b/frontend/src/app/components/ord-data/ord-data.component.html @@ -0,0 +1,65 @@ +@if (minted) { + + Mint + {{ minted >= 100000 ? (minted | amountShortener:undefined:undefined:true) : minted }} + + +} +@if (runestone?.etching?.supply) { + @if (runestone?.etching.premine > 0) { + + Premine + {{ getAmount(runestone.etching.premine, runestone.etching.divisibility || 0) >= 100000 ? (getAmount(runestone.etching.premine, runestone.etching.divisibility || 0) | amountShortener:undefined:undefined:true) : getAmount(runestone.etching.premine, runestone.etching.divisibility || 0) }} + {{ runestone.etching.symbol }} + {{ runestone.etching.spacedName }} + ({{ toNumber(runestone.etching.premine) / toNumber(runestone.etching.supply) * 100 | amountShortener:0}}% of total supply) + + } @else { + + Etching of + {{ runestone.etching.symbol }} + {{ runestone.etching.spacedName }} + + } +} +@if (transferredRunes?.length && type === 'vout') { +
+ + Transfer + + +
+} + +@if (inscriptions?.length && type === 'vin') { +
+} + +@if (!runestone && type === 'vout') { +
+} + +@if ((runestone && !minted && !runestone.etching?.supply && !transferredRunes?.length && type === 'vout') || (!inscriptions?.length && type === 'vin')) { + Error decoding data +} + + + {{ runeInfo[id]?.etching.symbol || '' }} + + {{ runeInfo[id]?.etching.spacedName }} + + \ No newline at end of file diff --git a/frontend/src/app/components/ord-data/ord-data.component.scss b/frontend/src/app/components/ord-data/ord-data.component.scss new file mode 100644 index 000000000..b218359d9 --- /dev/null +++ b/frontend/src/app/components/ord-data/ord-data.component.scss @@ -0,0 +1,35 @@ +.amount { + font-weight: bold; +} + +a.rune-link { + color: inherit; + &:hover { + text-decoration: underline; + text-decoration-color: var(--transparent-fg); + } +} + +a.disabled { + text-decoration: none; +} + +.name { + color: var(--transparent-fg); + font-weight: 700; +} + +.badge-ord { + background-color: var(--grey); + position: relative; + top: -2px; + font-size: 81%; + &.primary { + background-color: var(--primary); + } +} + +pre { + margin-top: 5px; + max-height: 200px; +} \ No newline at end of file diff --git a/frontend/src/app/components/ord-data/ord-data.component.ts b/frontend/src/app/components/ord-data/ord-data.component.ts new file mode 100644 index 000000000..4c0318718 --- /dev/null +++ b/frontend/src/app/components/ord-data/ord-data.component.ts @@ -0,0 +1,87 @@ +import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { Runestone, Etching } from '@app/shared/ord/rune.utils'; +import { Inscription } from '@app/shared/ord/inscription.utils'; + +@Component({ + selector: 'app-ord-data', + templateUrl: './ord-data.component.html', + styleUrls: ['./ord-data.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class OrdDataComponent implements OnChanges { + @Input() inscriptions: Inscription[]; + @Input() runestone: Runestone; + @Input() runeInfo: { [id: string]: { etching: Etching; txid: string } }; + @Input() type: 'vin' | 'vout'; + + toNumber = (value: bigint): number => Number(value); + + // Inscriptions + inscriptionsData: { [key: string]: { count: number, totalSize: number, text?: string; json?: JSON; tag?: string; delegate?: string } }; + // Rune mints + minted: number; + // Rune transfers + transferredRunes: { key: string; etching: Etching; txid: string }[] = []; + + constructor() { } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.runestone && this.runestone) { + if (this.runestone.mint && this.runeInfo[this.runestone.mint.toString()]) { + const mint = this.runestone.mint.toString(); + const terms = this.runeInfo[mint].etching.terms; + const amount = terms?.amount; + const divisibility = this.runeInfo[mint].etching.divisibility; + if (amount) { + this.minted = this.getAmount(amount, divisibility); + } + } + + this.runestone.edicts.forEach(edict => { + if (this.runeInfo[edict.id.toString()]) { + this.transferredRunes.push({ key: edict.id.toString(), ...this.runeInfo[edict.id.toString()] }); + } + }); + } + + if (changes.inscriptions && this.inscriptions) { + + if (this.inscriptions?.length) { + this.inscriptionsData = {}; + this.inscriptions.forEach((inscription) => { + // General: count, total size, delegate + const key = inscription.content_type_str || 'undefined'; + if (!this.inscriptionsData[key]) { + this.inscriptionsData[key] = { count: 0, totalSize: 0 }; + } + this.inscriptionsData[key].count++; + this.inscriptionsData[key].totalSize += inscription.body_length; + if (inscription.delegate_txid && !this.inscriptionsData[key].delegate) { + this.inscriptionsData[key].delegate = inscription.delegate_txid; + } + + // Text / JSON data + if ((key.includes('text') || key.includes('json')) && !inscription.is_cropped && !this.inscriptionsData[key].text && !this.inscriptionsData[key].json) { + const decoder = new TextDecoder('utf-8'); + const text = decoder.decode(inscription.body); + try { + this.inscriptionsData[key].json = JSON.parse(text); + if (this.inscriptionsData[key].json['p']) { + this.inscriptionsData[key].tag = this.inscriptionsData[key].json['p'].toUpperCase(); + } + } catch (e) { + this.inscriptionsData[key].text = text; + } + } + }); + } + } + } + + getAmount(amount: bigint, divisibility: number): number { + const divisor = BigInt(10) ** BigInt(divisibility); + const result = amount / divisor; + + return result <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(result) : Number.MAX_SAFE_INTEGER; + } +} diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.html b/frontend/src/app/components/pool-ranking/pool-ranking.component.html index 7600797cb..f6aa4d4b9 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.html +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.html @@ -90,9 +90,9 @@ Rank Pool - Hashrate + Hashrate Blocks - Avg Health Avg Block Fees Empty Blocks @@ -105,12 +105,13 @@ {{ pool.name }} - {{ pool.lastEstimatedHashrate | number: '1.2-2' }} {{ - miningStats.miningUnits.hashrateUnit }} + {{ pool.lastEstimatedHashrate | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }} + {{ pool.lastEstimatedHashrate3d | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }} + {{ pool.lastEstimatedHashrate1w | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }} {{ pool.blockCount }} ({{ pool.share }}%) - + All miners - {{ miningStats.lastEstimatedHashrate | number: '1.2-2' }} {{ - miningStats.miningUnits.hashrateUnit }} + {{ miningStats.lastEstimatedHashrate| number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }} + {{ miningStats.lastEstimatedHashrate3d | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }} + {{ miningStats.lastEstimatedHashrate1w | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }} {{ miningStats.blockCount }} diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.scss b/frontend/src/app/components/pool-ranking/pool-ranking.component.scss index 9001fc085..cf53ebe14 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.scss +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.scss @@ -60,7 +60,7 @@ position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; } .pool-distribution { diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts index 2e8a820be..de7f9b2e0 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts @@ -1,17 +1,17 @@ import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit, HostBinding } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { EChartsOption, PieSeriesOption } from '../../graphs/echarts'; +import { EChartsOption, PieSeriesOption } from '@app/graphs/echarts'; import { merge, Observable } from 'rxjs'; import { map, shareReplay, startWith, switchMap, tap } from 'rxjs/operators'; -import { SeoService } from '../../services/seo.service'; -import { StorageService } from '../..//services/storage.service'; -import { MiningService, MiningStats } from '../../services/mining.service'; -import { StateService } from '../../services/state.service'; -import { chartColors, poolsColor } from '../../app.constants'; -import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; -import { download } from '../../shared/graphs.utils'; -import { isMobile } from '../../shared/common.utils'; +import { SeoService } from '@app/services/seo.service'; +import { StorageService } from '@app//services/storage.service'; +import { MiningService, MiningStats } from '@app/services/mining.service'; +import { StateService } from '@app/services/state.service'; +import { chartColors, poolsColor } from '@app/app.constants'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; +import { download } from '@app/shared/graphs.utils'; +import { isMobile } from '@app/shared/common.utils'; @Component({ selector: 'app-pool-ranking', @@ -161,9 +161,12 @@ export class PoolRankingComponent implements OnInit { borderColor: '#000', formatter: () => { const i = pool.blockCount.toString(); - if (this.miningWindowPreference === '24h') { + if (['24h', '3d', '1w'].includes(this.miningWindowPreference)) { + let hashrate = pool.lastEstimatedHashrate; + if ('3d' === this.miningWindowPreference) { hashrate = pool.lastEstimatedHashrate3d; } + if ('1w' === this.miningWindowPreference) { hashrate = pool.lastEstimatedHashrate1w; } return `${pool.name} (${pool.share}%)
` + - pool.lastEstimatedHashrate.toFixed(2) + ' ' + miningStats.miningUnits.hashrateUnit + + hashrate.toFixed(2) + ' ' + miningStats.miningUnits.hashrateUnit + `
` + $localize`${ i }:INTERPOLATION: blocks`; } else { return `${pool.name} (${pool.share}%)
` + @@ -200,13 +203,10 @@ export class PoolRankingComponent implements OnInit { borderColor: '#000', formatter: () => { const i = totalBlockOther.toString(); - if (this.miningWindowPreference === '24h') { - return `` + $localize`Other (${percentage})` + `
` + - totalEstimatedHashrateOther.toString() + ' ' + miningStats.miningUnits.hashrateUnit + - `
` + $localize`${ i }:INTERPOLATION: blocks`; + if (['24h', '3d', '1w'].includes(this.miningWindowPreference)) { + return `` + $localize`Other (${percentage})` + `
` + totalEstimatedHashrateOther.toFixed(2) + ' ' + miningStats.miningUnits.hashrateUnit + `
` + $localize`${ i }:INTERPOLATION: blocks`; } else { - return `` + $localize`Other (${percentage})` + `
` + - $localize`${ i }:INTERPOLATION: blocks`; + return `` + $localize`Other (${percentage})` + `
` + $localize`${ i }:INTERPOLATION: blocks`; } } }, @@ -292,6 +292,8 @@ export class PoolRankingComponent implements OnInit { getEmptyMiningStat(): MiningStats { return { lastEstimatedHashrate: 0, + lastEstimatedHashrate3d: 0, + lastEstimatedHashrate1w: 0, blockCount: 0, totalEmptyBlock: 0, totalEmptyBlockRatio: '', diff --git a/frontend/src/app/components/pool/pool-preview.component.ts b/frontend/src/app/components/pool/pool-preview.component.ts index e0c786082..93077120d 100644 --- a/frontend/src/app/components/pool/pool-preview.component.ts +++ b/frontend/src/app/components/pool/pool-preview.component.ts @@ -1,14 +1,14 @@ import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { echarts, EChartsOption } from '../../graphs/echarts'; +import { echarts, EChartsOption } from '@app/graphs/echarts'; import { Observable, of } from 'rxjs'; import { map, switchMap, catchError } from 'rxjs/operators'; -import { PoolStat } from '../../interfaces/node-api.interface'; -import { ApiService } from '../../services/api.service'; -import { StateService } from '../../services/state.service'; +import { PoolStat } from '@interfaces/node-api.interface'; +import { ApiService } from '@app/services/api.service'; +import { StateService } from '@app/services/state.service'; import { formatNumber } from '@angular/common'; -import { SeoService } from '../../services/seo.service'; -import { OpenGraphService } from '../../services/opengraph.service'; +import { SeoService } from '@app/services/seo.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; @Component({ selector: 'app-pool-preview', diff --git a/frontend/src/app/components/pool/pool.component.scss b/frontend/src/app/components/pool/pool.component.scss index 36bdc93e9..5c2fedd26 100644 --- a/frontend/src/app/components/pool/pool.component.scss +++ b/frontend/src/app/components/pool/pool.component.scss @@ -167,7 +167,7 @@ div.scrollable { .loadingGraphs { position: absolute; left: calc(50% - 15px); - z-index: 100; + z-index: 99; top: 475px; @media (max-width: 992px) { top: 600px; diff --git a/frontend/src/app/components/pool/pool.component.ts b/frontend/src/app/components/pool/pool.component.ts index 6564a5dd9..1893f0a48 100644 --- a/frontend/src/app/components/pool/pool.component.ts +++ b/frontend/src/app/components/pool/pool.component.ts @@ -1,14 +1,14 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { echarts, EChartsOption } from '../../graphs/echarts'; +import { echarts, EChartsOption } from '@app/graphs/echarts'; import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs'; import { catchError, distinctUntilChanged, filter, map, share, switchMap, tap } from 'rxjs/operators'; -import { BlockExtended, PoolStat } from '../../interfaces/node-api.interface'; -import { ApiService } from '../../services/api.service'; -import { StateService } from '../../services/state.service'; -import { selectPowerOfTen } from '../../bitcoin.utils'; +import { BlockExtended, PoolStat } from '@interfaces/node-api.interface'; +import { ApiService } from '@app/services/api.service'; +import { StateService } from '@app/services/state.service'; +import { selectPowerOfTen } from '@app/bitcoin.utils'; import { formatNumber } from '@angular/common'; -import { SeoService } from '../../services/seo.service'; +import { SeoService } from '@app/services/seo.service'; import { HttpErrorResponse } from '@angular/common/http'; interface AccelerationTotal { diff --git a/frontend/src/app/components/privacy-policy/privacy-policy.component.html b/frontend/src/app/components/privacy-policy/privacy-policy.component.html index effdf29f2..06b09ad30 100644 --- a/frontend/src/app/components/privacy-policy/privacy-policy.component.html +++ b/frontend/src/app/components/privacy-policy/privacy-policy.component.html @@ -5,7 +5,7 @@

Privacy Policy

-
Updated: July 10, 2024
+
Updated: July 31, 2024


@@ -27,7 +27,7 @@
-

General

+

USING THIS WEBSITE

Out of respect for the Bitcoin community, this Website does not use any third-party analytics, third-party trackers, or third-party cookies, and we do not share any private user data with third-parties. Additionally, to mitigate the risk of surveillance by malicious third-parties, we self-host this Website on our own hardware and network infrastructure, so there are no "hosting companies" or "cloud providers" involved with the operation of this Website.

@@ -35,7 +35,7 @@
\ 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.html b/frontend/src/app/components/server-health/server-health.component.html index e2f76d786..6a0a905f9 100644 --- a/frontend/src/app/components/server-health/server-health.component.html +++ b/frontend/src/app/components/server-health/server-health.component.html @@ -9,7 +9,7 @@
- +
@@ -27,7 +27,7 @@ - +
{{ getLastUpdateSeconds(host) }} {{ (host.rtt / 1000) | number : '1.1-1' }} {{ host.rtt == null ? '' : 's'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }} {{ host.rtt | number : '1.0-0' }} {{ host.rtt == null ? '' : 'ms'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < tip ? '🟧' : '✅')) }}{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '✅')) }}
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 37e23f12a..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 { Observable, Subject, map } from 'rxjs'; -import { StateService } from '../../services/state.service'; -import { HealthCheckHost } from '../../interfaces/websocket.interface'; +import { WebsocketService } from '@app/services/websocket.service'; +import { Observable, Subject, map, tap } from 'rxjs'; +import { StateService } from '@app/services/state.service'; +import { HealthCheckHost } from '@interfaces/websocket.interface'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ @@ -13,7 +13,7 @@ import { DomSanitizer } from '@angular/platform-browser'; }) export class ServerHealthComponent implements OnInit { hosts$: Observable; - tip$: Subject; + maxHeight: number; interval: number; now: number = Date.now(); @@ -44,9 +44,14 @@ export class ServerHealthComponent implements OnInit { host.flag = this.parseFlag(host.host); } return hosts; + }), + tap(hosts => { + let newMaxHeight = 0; + for (const host of hosts) { + newMaxHeight = Math.max(newMaxHeight, host.latestHeight); + } }) ); - this.tip$ = this.stateService.chainTip$; this.websocketService.want(['mempool-blocks', 'stats', 'blocks', 'tomahawk']); this.interval = window.setInterval(() => { 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 d92838fc5..2d9bd4982 100644 --- a/frontend/src/app/components/tracker/tracker.component.html +++ b/frontend/src/app/components/tracker/tracker.component.html @@ -42,11 +42,11 @@
-
+
@if (replaced) { } @else {
@@ -61,27 +61,29 @@ @if (transactionTime > 0) { } @else { - + }
-
-
ETA
-
- - - @if (eta.blocks >= 7) { - Not any time soon - } @else { - - } - - - - - -
-
+ @if (!replaced) { +
+
ETA
+
+ + + @if (eta.blocks >= 7) { + Not any time soon + } @else { + + } + + + + + +
+
+ } } @else if (tx && tx.status?.confirmed) {
Confirmed at
@@ -111,14 +113,14 @@
-
+
@if (isLoading) {
  } @else { - @if (!tx.status?.confirmed && showAccelerationSummary) { + @if (tx && !tx.status?.confirmed && showAccelerationSummary) { } -
+
@if (tx?.acceleration && !tx.status?.confirmed) {
@@ -184,8 +186,18 @@
}
+ +
+ + Error loading transaction data. + +
- diff --git a/frontend/src/app/components/tracker/tracker.component.scss b/frontend/src/app/components/tracker/tracker.component.scss index 56f0b5f0a..614691426 100644 --- a/frontend/src/app/components/tracker/tracker.component.scss +++ b/frontend/src/app/components/tracker/tracker.component.scss @@ -49,7 +49,7 @@ position: relative; background: var(--nav-bg); box-shadow: 0 -5px 15px #000; - z-index: 100; + z-index: 99; align-items: center; justify-content: space-between; diff --git a/frontend/src/app/components/tracker/tracker.component.ts b/frontend/src/app/components/tracker/tracker.component.ts index 4ba00c189..5ee27771d 100644 --- a/frontend/src/app/components/tracker/tracker.component.ts +++ b/frontend/src/app/components/tracker/tracker.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, OnDestroy, HostListener, Inject, ChangeDetectorRef, ChangeDetectionStrategy, NgZone } 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, @@ -12,25 +12,27 @@ import { map, startWith } from 'rxjs/operators'; -import { Transaction } from '../../interfaces/electrs.interface'; +import { Transaction } from '@interfaces/electrs.interface'; import { of, merge, Subscription, Observable, Subject, 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 { seoDescriptionNetwork } from '../../shared/common.utils'; -import { Filter, TransactionFlags } from '../../shared/filters.utils'; -import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration, AccelerationPosition } from '../../interfaces/node-api.interface'; -import { PriceService } from '../../services/price.service'; -import { ServicesApiServices } from '../../services/services-api.service'; -import { EnterpriseService } from '../../services/enterprise.service'; -import { ZONE_SERVICE } from '../../injection-tokens'; -import { TrackerStage } from './tracker-bar.component'; -import { MiningService, MiningStats } from '../../services/mining.service'; -import { ETA, EtaService } from '../../services/eta.service'; -import { getTransactionFlags, getUnacceleratedFeeRate } from '../../shared/transaction.utils'; +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 { seoDescriptionNetwork } from '@app/shared/common.utils'; +import { Filter, TransactionFlags } from '@app/shared/filters.utils'; +import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration, AccelerationPosition } from '@interfaces/node-api.interface'; +import { PriceService } from '@app/services/price.service'; +import { ServicesApiServices } from '@app/services/services-api.service'; +import { EnterpriseService } from '@app/services/enterprise.service'; +import { ZONE_SERVICE } from '@app/injection-tokens'; +import { TrackerStage } from '@components/tracker/tracker-bar.component'; +import { MiningService, MiningStats } from '@app/services/mining.service'; +import { ETA, EtaService } from '@app/services/eta.service'; +import { getTransactionFlags, getUnacceleratedFeeRate } from '@app/shared/transaction.utils'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; + interface Pool { id: number; @@ -140,6 +142,7 @@ export class TrackerComponent implements OnInit, OnDestroy { private priceService: PriceService, private enterpriseService: EnterpriseService, private miningService: MiningService, + private router: Router, private cd: ChangeDetectorRef, private zone: NgZone, @Inject(ZONE_SERVICE) private zoneService: any, @@ -283,14 +286,14 @@ export class TrackerComponent implements OnInit, OnDestroy { this.accelerationInfo = null; }), switchMap((blockHash: string) => { - return this.servicesApiService.getAccelerationHistory$({ blockHash }); + return this.servicesApiService.getAllAccelerationHistory$({ blockHash }, null, this.txId); }), catchError(() => { return of(null); }) ).subscribe((accelerationHistory) => { for (const acceleration of accelerationHistory) { - if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) { + if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) { const boostCost = acceleration.boostCost || acceleration.bidBoost; acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; acceleration.boost = boostCost; @@ -744,7 +747,7 @@ export class TrackerComponent implements OnInit, OnDestroy { checkAccelerationEligibility() { if (this.tx) { - this.tx.flags = getTransactionFlags(this.tx); + this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network); const replaceableInputs = (this.tx.flags & (TransactionFlags.sighash_none | TransactionFlags.sighash_acp)) > 0n; const highSigop = (this.tx.sigops * 20) > this.tx.weight; this.eligibleForAcceleration = !replaceableInputs && !highSigop; diff --git a/frontend/src/app/components/tracker/tracker.module.ts b/frontend/src/app/components/tracker/tracker.module.ts index 799b8cd65..b41a68bc5 100644 --- a/frontend/src/app/components/tracker/tracker.module.ts +++ b/frontend/src/app/components/tracker/tracker.module.ts @@ -1,12 +1,12 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Routes, RouterModule } from '@angular/router'; -import { SharedModule } from '../../shared/shared.module'; -import { TxBowtieModule } from '../tx-bowtie-graph/tx-bowtie.module'; -import { GraphsModule } from '../../graphs/graphs.module'; -import { TrackerComponent } from '../tracker/tracker.component'; -import { TrackerBarComponent } from '../tracker/tracker-bar.component'; -import { TransactionModule } from '../transaction/transaction.module'; +import { SharedModule } from '@app/shared/shared.module'; +import { TxBowtieModule } from '@components/tx-bowtie-graph/tx-bowtie.module'; +import { GraphsModule } from '@app/graphs/graphs.module'; +import { TrackerComponent } from '@components/tracker/tracker.component'; +import { TrackerBarComponent } from '@components/tracker/tracker-bar.component'; +import { TransactionModule } from '@components/transaction/transaction.module'; const routes: Routes = [ { diff --git a/frontend/src/app/components/trademark-policy/trademark-policy.component.html b/frontend/src/app/components/trademark-policy/trademark-policy.component.html index de1d78daa..e12cbb8b2 100644 --- a/frontend/src/app/components/trademark-policy/trademark-policy.component.html +++ b/frontend/src/app/components/trademark-policy/trademark-policy.component.html @@ -8,7 +8,7 @@

Trademark Policy and Guidelines

The Mempool Open Source Project ®
-
Updated: July 3, 2024
+
Updated: August 19, 2024

@@ -95,16 +95,31 @@

The mempool Square Logo



- +

The Mempool Accelerator Logo



+ +

+

The mempool research Logo

+

+

The Mempool Goggles Logo



+ +

+

The mempool transaction Logo

+

+ + +

+

The mempool block visualization Logo

+

+

The mempool Blocks Logo

diff --git a/frontend/src/app/components/trademark-policy/trademark-policy.component.ts b/frontend/src/app/components/trademark-policy/trademark-policy.component.ts index ad8b6b372..d27848c55 100644 --- a/frontend/src/app/components/trademark-policy/trademark-policy.component.ts +++ b/frontend/src/app/components/trademark-policy/trademark-policy.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-trademark-policy', diff --git a/frontend/src/app/components/trademark-policy/trademark-policy.module.ts b/frontend/src/app/components/trademark-policy/trademark-policy.module.ts index 24f70be52..eaf57a5a2 100644 --- a/frontend/src/app/components/trademark-policy/trademark-policy.module.ts +++ b/frontend/src/app/components/trademark-policy/trademark-policy.module.ts @@ -1,8 +1,8 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Routes, RouterModule } from '@angular/router'; -import { TrademarkPolicyComponent } from './trademark-policy.component'; -import { SharedModule } from '../../shared/shared.module'; +import { TrademarkPolicyComponent } from '@components/trademark-policy/trademark-policy.component'; +import { SharedModule } from '@app/shared/shared.module'; const routes: Routes = [ { diff --git a/frontend/src/app/components/transaction/liquid-ublinding.ts b/frontend/src/app/components/transaction/liquid-ublinding.ts index a53edeb4c..259b06a0b 100644 --- a/frontend/src/app/components/transaction/liquid-ublinding.ts +++ b/frontend/src/app/components/transaction/liquid-ublinding.ts @@ -1,4 +1,4 @@ -import { Transaction } from '../../interfaces/electrs.interface'; +import { Transaction } from '@interfaces/electrs.interface'; // Parse the blinders data from a string encoded as a comma separated list, in the following format: // ,,, @@ -34,7 +34,7 @@ export class LiquidUnblinding { } async makeCommitmentMap(blinders: any) { - const libwally = await import('./libwally.js'); + const libwally = await import('@components/transaction/libwally.js'); await libwally.load(); const commitments = new Map(); blinders.forEach(b => { @@ -140,4 +140,4 @@ export class LiquidUnblinding { } return tx; } -} \ No newline at end of file +} diff --git a/frontend/src/app/components/transaction/transaction-details/transaction-details.component.html b/frontend/src/app/components/transaction/transaction-details/transaction-details.component.html new file mode 100644 index 000000000..0bfcb494e --- /dev/null +++ b/frontend/src/app/components/transaction/transaction-details/transaction-details.component.html @@ -0,0 +1,324 @@ +
+
+ @if (isMobile) { +
+ + + + + +
+
+ } @else { +
+ + + + +
+
+
+ + + + +
+
+ } +
+
+ + + @if (tx?.status?.confirmed) { + + + } @else { + + + } + + @if (tx?.status?.confirmed) { + + } + + + + + + + @if (!isLoadingTx && !tx?.status?.confirmed && isAcceleration && ((cpfpInfo && hasEffectiveFeeRate) || accelerationInfo)) { + + } @else { + + } + @if (tx?.status?.confirmed) { + + } + + + + @if (!isLoadingTx) { + + Timestamp + + ‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }} +
+ () +
+ + + } @else { + + } +
+ + + @if (!isLoadingTx) { + @if (transactionTime > 0) { + + Confirmed + + + } + } @else { + + } + + + + @if (isLoadingTx) { + + } @else if (transactionTime > 0) { + + First seen + + + } @else if (isLoadingFirstSeen) { + + First seen + + + } + + + + @if (network !== 'liquid' && network !== 'liquidtestnet') { + @if (!isLoadingTx) { + @if (featuresEnabled) { + + Features + + + + + } + } @else { + + } + } + + + + @if (network === '') { + @if (!isLoadingTx) { + @if (auditStatus) { + + Audit + + + @if (auditStatus.coinbase) { + Coinbase + } @else if (auditStatus.expected) { + Expected in Block + } @else if (auditStatus.seen) { + Seen in Mempool + } @else if (!auditStatus.conflict) { + Not seen in Mempool + } + @if (auditStatus.added) { + Added + } + @if (auditStatus.prioritized) { + Prioritized + } + @if (auditStatus.conflict) { + Conflict + } + + + + } + } @else { + + } + } + + + + @if (!isLoadingTx) { + @if (!replaced && !isCached) { + + ETA + + + @if (network === 'liquid' || network === 'liquidtestnet') { + + } @else { + + @if (eta.blocks >= 7) { + Not any time soon + } @else { + + } + @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && notAcceleratedOnLoad) { + + } + + } + + + + + + + } + } @else { + + } + + + + @if (!isLoadingTx) { + @if (isAcceleration || filters.length) { + + + + + + @if (isAcceleration) { + Accelerated + } + + {{ filter.label }} + + + + } + } + + + + @if (!isLoadingTx) { + + Fee + {{ tx.fee | number }} sats + @if (isAcceleration && accelerationInfo?.bidBoost ?? tx.feeDelta > 0) { + +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} sats + } + + + + } @else { + + } + + + + @if (!isLoadingTx) { + + Fee rate + + + @if (tx?.status?.confirmed && tx.fee && !hasEffectiveFeeRate && !accelerationInfo) { +   + + } + + + } @else { + + } + + + + @if (!isLoadingTx) { + @if ((cpfpInfo && hasEffectiveFeeRate) || (accelerationInfo && isAcceleration)) { + + @if (isAcceleration) { + Accelerated fee rate + } @else { + Effective fee rate + } + +
+ @if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize || tx.acceleration)) { + + } @else { + + } + + @if (tx?.status?.confirmed && !tx.acceleration && !accelerationInfo && tx.fee && tx.effectiveFeePerVsize) { + + } +
+ @if (hasCpfp) { + + } + + + } + } @else { + + } +
+ + + + + + + + + + + + @if (network === '') { + @if (!isLoadingTx) { + + Miner + @if (pool) { + + + + @if (pool.minerNames[1].length > 16) { + {{ pool.minerNames[1].slice(0, 15) }}… + } @else { + {{ pool.minerNames[1] }} + } + + + {{ pool.name }} + + + } @else { + + + + } + + } @else { + + } + } + + + + + + + \ No newline at end of file diff --git a/frontend/src/app/components/transaction/transaction-details/transaction-details.component.scss b/frontend/src/app/components/transaction/transaction-details/transaction-details.component.scss new file mode 100644 index 000000000..9bb32ba4a --- /dev/null +++ b/frontend/src/app/components/transaction/transaction-details/transaction-details.component.scss @@ -0,0 +1,183 @@ +.title-block { + flex-wrap: wrap; + align-items: baseline; + @media (min-width: 650px) { + flex-direction: row; + } + h1 { + margin: 0rem; + margin-right: 15px; + line-height: 1; + } +} + +.td-width { + width: 150px; + + @media (max-width: 768px) { + width: 175px; + } +} + +.badge { + position: relative; + top: -1px; +} + +.miner-name { + margin-right: 4px; + vertical-align: top; +} + +.pool-logo { + width: 25px; + height: 25px; + position: relative; + top: -1px; + margin-right: 2px; +} + +.badge.badge-accelerated { + background-color: var(--tertiary); + color: white; +} + +.btn-small-height { + line-height: 1; +} + +.row{ + flex-direction: column; + @media (min-width: 850px) { + flex-direction: row; + } +} + +.box.hidden { + visibility: hidden; + height: 0px; + padding-top: 0px; + padding-bottom: 0px; + margin-top: 0px; + margin-bottom: 0px; +} + +@media (max-width: 767.98px) { + .mobile-bottomcol { + margin-top: 15px; + } + + .details-table td:first-child { + white-space: pre-wrap; + } +} + +.fiat { + display: block; + @media (min-width: 768px){ + display: inline-block; + margin-left: 15px; + text-align: left; + } +} + +.table { + tr td { + padding: 0.75rem 0.5rem; + @media (min-width: 576px) { + padding: 0.75rem 0.75rem; + } + &:last-child { + text-align: right; + @media (min-width: 850px) { + text-align: left; + } + } + .btn { + display: block; + } + + &.wrap-cell { + white-space: normal; + } + } +} + +.effective-fee-container { + display: block; + @media (min-width: 768px){ + display: inline-block; + } + @media (max-width: 425px){ + display: flex; + flex-direction: column; + } +} + +@media (max-width: 767px){ + .hide-on-mobile { + display: none; + } +} + +.effective-fee-rating { + @media (max-width: 767px){ + margin-right: 0px !important; + } +} + +.btn-outline-info { + margin-top: 5px; + @media (min-width: 768px){ + margin-top: 0px; + } +} + +.eta { + display: flex; + flex-wrap: wrap; + align-content: center; + @media (min-width: 850px) { + justify-content: left !important; + } +} + +.accelerate { + @media (min-width: 850px) { + margin-left: auto; + } +} + +.etaDeepMempool { + flex-wrap: wrap; + @media (max-width: 849px) { + justify-content: right !important; + } +} + +.accelerateDeepMempool { + background-color: var(--tertiary); + margin-left: 5px; +} + +.goggles-icon { + display: block; + width: 2.7em; +} + +.pool-logo { + width: 15px; + height: 15px; + position: relative; + top: -1px; + margin-right: 2px; +} + +.oobFees { + color: #905cf4; +} + +.disabled { + opacity: 0.5; + pointer-events: none; +} diff --git a/frontend/src/app/components/transaction/transaction-details/transaction-details.component.ts b/frontend/src/app/components/transaction/transaction-details/transaction-details.component.ts new file mode 100644 index 000000000..2b539c154 --- /dev/null +++ b/frontend/src/app/components/transaction/transaction-details/transaction-details.component.ts @@ -0,0 +1,56 @@ +import { Component, OnInit, Input, ChangeDetectionStrategy, Output, EventEmitter } from '@angular/core'; +import { Transaction } from '@interfaces/electrs.interface'; +import { Acceleration, CpfpInfo } from '@interfaces/node-api.interface'; +import { Pool, TxAuditStatus } from '@components/transaction/transaction.component'; +import { Observable } from 'rxjs'; +import { ETA } from '@app/services/eta.service'; +import { MiningStats } from '@app/services/mining.service'; +import { Filter } from '@app/shared/filters.utils'; + +@Component({ + selector: 'app-transaction-details', + templateUrl: './transaction-details.component.html', + styleUrls: ['./transaction-details.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TransactionDetailsComponent implements OnInit { + @Input() network: string; + @Input() tx: Transaction; + @Input() isLoadingTx: boolean; + @Input() isMobile: boolean; + @Input() transactionTime: number; + @Input() isLoadingFirstSeen: boolean; + @Input() featuresEnabled: boolean; + @Input() auditStatus: TxAuditStatus; + @Input() filters: Filter[]; + @Input() miningStats: MiningStats; + @Input() pool: Pool | null; + @Input() isAcceleration: boolean; + @Input() hasEffectiveFeeRate: boolean; + @Input() cpfpInfo: CpfpInfo; + @Input() hasCpfp: boolean; + @Input() accelerationInfo: Acceleration; + @Input() acceleratorAvailable: boolean; + @Input() accelerateCtaType: string; + @Input() notAcceleratedOnLoad: boolean; + @Input() showAccelerationSummary: boolean; + @Input() eligibleForAcceleration: boolean; + @Input() replaced: boolean; + @Input() isCached: boolean; + @Input() ETA$: Observable; + + @Output() accelerateClicked = new EventEmitter(); + @Output() toggleCpfp$ = new EventEmitter(); + + constructor() {} + + ngOnInit(): void {} + + onAccelerateClicked(): void { + this.accelerateClicked.emit(true); + } + + toggleCpfp(): void { + this.toggleCpfp$.emit(); + } +} diff --git a/frontend/src/app/components/transaction/transaction-extras.module.ts b/frontend/src/app/components/transaction/transaction-extras.module.ts new file mode 100644 index 000000000..ddfff51a5 --- /dev/null +++ b/frontend/src/app/components/transaction/transaction-extras.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; + +@NgModule({ + declarations: [ + ], + imports: [ + ], + exports: [ + ] +}) +export class TransactionExtrasModule { } diff --git a/frontend/src/app/components/transaction/transaction-preview.component.html b/frontend/src/app/components/transaction/transaction-preview.component.html index 63a11a8f0..066e0d442 100644 --- a/frontend/src/app/components/transaction/transaction-preview.component.html +++ b/frontend/src/app/components/transaction/transaction-preview.component.html @@ -21,7 +21,7 @@ ‎{{ transactionTime * 1000 | date:'yyyy-MM-dd HH:mm' }} - Fee {{ tx.fee | number }} sat + Fee {{ tx.fee | number }} sats
diff --git a/frontend/src/app/components/transaction/transaction-preview.component.ts b/frontend/src/app/components/transaction/transaction-preview.component.ts index 313442dbf..0c51e0064 100644 --- a/frontend/src/app/components/transaction/transaction-preview.component.ts +++ b/frontend/src/app/components/transaction/transaction-preview.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; -import { ElectrsApiService } from '../../services/electrs-api.service'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { switchMap, @@ -8,16 +8,16 @@ import { retryWhen, delay, } from 'rxjs/operators'; -import { Transaction, Vout } from '../../interfaces/electrs.interface'; +import { Transaction, Vout } from '@interfaces/electrs.interface'; import { of, merge, Subscription, Observable, Subject, from } from 'rxjs'; -import { StateService } from '../../services/state.service'; -import { CacheService } from '../../services/cache.service'; -import { OpenGraphService } from '../../services/opengraph.service'; -import { ApiService } from '../../services/api.service'; -import { SeoService } from '../../services/seo.service'; -import { seoDescriptionNetwork } from '../../shared/common.utils'; -import { CpfpInfo } from '../../interfaces/node-api.interface'; -import { LiquidUnblinding } from './liquid-ublinding'; +import { StateService } from '@app/services/state.service'; +import { CacheService } from '@app/services/cache.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; +import { ApiService } from '@app/services/api.service'; +import { SeoService } from '@app/services/seo.service'; +import { seoDescriptionNetwork } from '@app/shared/common.utils'; +import { CpfpInfo } from '@interfaces/node-api.interface'; +import { LiquidUnblinding } from '@components/transaction/liquid-ublinding'; @Component({ selector: 'app-transaction-preview', diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 9ce22d26c..8c2d9de01 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -31,43 +31,46 @@
@if (!error) { -
-
- @if (isMobile) { -
- - - - - -
-
- } @else { -
- - - - -
-
-
- - - - -
-
- } -
-
+ } + +
-

CPFP

+
+

Related Transactions

+
@@ -162,12 +165,12 @@
- +

Acceleration Timeline

- +
@@ -414,287 +417,4 @@ - - - - @if (tx?.status?.confirmed) { - - - } @else { - - - } - - @if (tx?.status?.confirmed) { - - } - - - - - - - @if (!isLoadingTx && !tx?.status?.confirmed && isAcceleration && ((cpfpInfo && hasEffectiveFeeRate) || accelerationInfo)) { - - } @else { - - } - @if (tx?.status?.confirmed) { - - } - - - - @if (!isLoadingTx) { -
- - - - } @else { - - } - - - - @if (!isLoadingTx) { - @if (transactionTime > 0) { - - - - - } - } @else { - - } - - - - @if (isLoadingTx) { - - } @else if (transactionTime > 0) { - - - - - } @else if (isLoadingFirstSeen) { - - - - - } - - - - @if (network !== 'liquid' && network !== 'liquidtestnet') { - @if (!isLoadingTx) { - @if (featuresEnabled) { - - - - - } - } @else { - - } - } - - - - @if (network === '') { - @if (!isLoadingTx) { - @if (auditStatus) { - - - - - } - } @else { - - } - } - - - - @if (!isLoadingTx) { - @if (!replaced && !isCached) { - - - - - } - } @else { - - } - - - - @if (!isLoadingTx) { - @if (isAcceleration || filters.length) { - - - - - } - } - - - - @if (!isLoadingTx) { - - - - - } @else { - - } - - - - @if (!isLoadingTx) { - - - - - } @else { - - } - - - - @if (!isLoadingTx) { - @if ((cpfpInfo && hasEffectiveFeeRate) || accelerationInfo) { - - @if (isAcceleration) { - - } @else { - - } - - - } - } @else { - - } - - - - - - - - - - - @if (network === '') { - @if (!isLoadingTx) { - - - @if (pool) { - - } @else { - - } - - } @else { - - } - } - - - - - - - \ No newline at end of file + \ 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 7db417126..d35f26130 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; @@ -65,10 +66,6 @@ color: white; } -.btn-small-height { - line-height: 1; -} - .arrow-green { color: var(--success); } @@ -81,7 +78,7 @@ margin-bottom: 40px; } -.row{ +.row { flex-direction: column; @media (min-width: 850px) { flex-direction: row; @@ -287,38 +284,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 { - display: flex !important; - 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 { @@ -333,3 +313,12 @@ top: -1px; margin-right: 2px; } + +.oobFees { + color: #905cf4; +} + +.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 924addaa0..71ffaa2cd 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 { @@ -106,6 +107,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { pool: Pool | null; auditStatus: TxAuditStatus | null; isAcceleration: boolean = false; + accelerationCanceled: boolean = false; filters: Filter[] = []; showCpfpDetails = false; miningStats: MiningStats; @@ -118,7 +120,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 +140,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { firstLoad = true; waitingForAccelerationInfo: boolean = false; isLoadingFirstSeen = false; + notAcceleratedOnLoad: boolean = null; featuresEnabled: boolean; segwitEnabled: boolean; @@ -191,7 +193,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.hideAccelerationSummary = this.stateService.isMempoolSpaceBuild ? this.storageService.getValue('hide-accelerator-pref') == 'true' : true; if (!this.stateService.isLiquid()) { - this.miningService.getMiningStats('1w').subscribe(stats => { + this.miningService.getMiningStats('1m').subscribe(stats => { this.miningStats = stats; }); } @@ -343,7 +345,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.setIsAccelerated(); }), switchMap((blockHeight: number) => { - return this.servicesApiService.getAccelerationHistory$({ blockHeight }).pipe( + return this.servicesApiService.getAllAccelerationHistory$({ blockHeight }, null, this.txId).pipe( switchMap((accelerationHistory: Acceleration[]) => { if (this.tx.acceleration && !accelerationHistory.length) { // If the just mined transaction was accelerated, but services backend did not return any acceleration data, retry return throwError('retry'); @@ -358,12 +360,19 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { }), ).subscribe((accelerationHistory) => { for (const acceleration of accelerationHistory) { - if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) { - const boostCost = acceleration.boostCost || acceleration.bidBoost; - acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; - acceleration.boost = boostCost; - this.tx.acceleratedAt = acceleration.added; - this.accelerationInfo = acceleration; + if (acceleration.txid === this.txId) { + if ((acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) { + const boostCost = acceleration.boostCost || acceleration.bidBoost; + acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; + acceleration.boost = boostCost; + this.tx.acceleratedAt = acceleration.added; + this.accelerationInfo = acceleration; + } + if (acceleration.status === 'failed' || acceleration.status === 'failed_provisional') { + this.accelerationCanceled = true; + this.tx.acceleratedAt = acceleration.added; + this.accelerationInfo = acceleration; + } this.waitingForAccelerationInfo = false; this.setIsAccelerated(); } @@ -399,6 +408,30 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { const auditAvailable = this.isAuditAvailable(height); const isCoinbase = this.tx.vin.some(v => v.is_coinbase); const fetchAudit = auditAvailable && !isCoinbase; + + const addFirstSeen = (audit: TxAuditStatus | null, hash: string, height: number, txid: string, useFullSummary: boolean) => { + if ( + this.isFirstSeenAvailable(height) + && !audit?.firstSeen // firstSeen is not already in audit + && (!audit || audit?.seen) // audit is disabled or tx is already seen (meaning 'firstSeen' is in block summary) + ) { + return useFullSummary ? + this.apiService.getStrippedBlockTransactions$(hash).pipe( + map(strippedTxs => { + return { audit, firstSeen: strippedTxs.find(tx => tx.txid === txid)?.time }; + }), + catchError(() => of({ audit })) + ) : + this.apiService.getStrippedBlockTransaction$(hash, txid).pipe( + map(strippedTx => { + return { audit, firstSeen: strippedTx?.time }; + }), + catchError(() => of({ audit })) + ); + } + return of({ audit }); + }; + if (fetchAudit) { // If block audit is already cached, use it to get transaction audit const blockAuditLoaded = this.apiService.getBlockAuditLoaded(hash); @@ -411,33 +444,41 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { const isConflict = audit.fullrbfTxs.includes(txid); const isExpected = audit.template.some(tx => tx.txid === txid); const firstSeen = audit.template.find(tx => tx.txid === txid)?.time; + const wasSeen = audit.version === 1 ? !audit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated); return { - seen: isExpected || isPrioritized || isAccelerated, + seen: wasSeen, expected: isExpected, - added: isAdded, + added: isAdded && (audit.version === 0 || !wasSeen), prioritized: isPrioritized, conflict: isConflict, accelerated: isAccelerated, firstSeen, }; + }), + switchMap(audit => addFirstSeen(audit, hash, height, txid, true)), + catchError(() => { + return of({ audit: null }); }) ) } else { return this.apiService.getBlockTxAudit$(hash, txid).pipe( retry({ count: 3, delay: 2000 }), + switchMap(audit => addFirstSeen(audit, hash, height, txid, false)), catchError(() => { - return of(null); + return of({ audit: null }); }) ) } } else { - return of(isCoinbase ? { coinbase: true } : null); + const audit = isCoinbase ? { coinbase: true } : null; + return addFirstSeen(audit, hash, height, txid, this.apiService.getBlockSummaryLoaded(hash)); } }), ).subscribe(auditStatus => { - this.auditStatus = auditStatus; - if (this.auditStatus?.firstSeen) { - this.transactionTime = this.auditStatus.firstSeen; + this.auditStatus = auditStatus?.audit; + const firstSeen = this.auditStatus?.firstSeen || auditStatus['firstSeen']; + if (firstSeen) { + this.transactionTime = firstSeen; } this.setIsAccelerated(); }); @@ -461,15 +502,29 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { if (txPosition.position.acceleratedBy) { txPosition.cpfp.acceleratedBy = txPosition.position.acceleratedBy; } + if (txPosition.position.acceleratedAt) { + txPosition.cpfp.acceleratedAt = txPosition.position.acceleratedAt; + } + if (txPosition.position.feeDelta) { + txPosition.cpfp.feeDelta = txPosition.position.feeDelta; + } this.setCpfpInfo(txPosition.cpfp); - } else if ((this.tx?.acceleration && txPosition.position.acceleratedBy)) { - this.tx.acceleratedBy = txPosition.position.acceleratedBy; + } else if ((this.tx?.acceleration)) { + if (txPosition.position.acceleratedBy) { + this.tx.acceleratedBy = txPosition.position.acceleratedBy; + } + if (txPosition.position.acceleratedAt) { + this.tx.acceleratedAt = txPosition.position.acceleratedAt; + } + if (txPosition.position.feeDelta) { + this.tx.feeDelta = txPosition.position.feeDelta; + } } 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; }); } @@ -518,6 +573,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { }); } } + if (window.innerWidth <= 767.98) { + this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.txId], { + queryParamsHandling: 'merge', + preserveFragment: true, + queryParams: { mode: 'details' }, + replaceUrl: true, + }); + } this.seoService.setTitle( $localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:` ); @@ -622,8 +685,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { ancestors: tx.ancestors, bestDescendant: tx.bestDescendant, }); - const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant); - this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && (Math.abs(tx.effectiveFeePerVsize - tx.feePerVsize) >= 0.1)); + const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant || tx.descendants); + this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && tx.effectiveFeePerVsize !== (this.tx.fee / (this.tx.weight / 4)) && tx.effectiveFeePerVsize !== (tx.fee / Math.ceil(tx.weight / 4))); } else { this.fetchCpfp$.next(this.tx.txid); } @@ -816,7 +879,19 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.tx.acceleration = cpfpInfo.acceleration; this.tx.acceleratedBy = cpfpInfo.acceleratedBy; this.tx.acceleratedAt = cpfpInfo.acceleratedAt; + this.tx.feeDelta = cpfpInfo.feeDelta; + this.accelerationCanceled = false; this.setIsAccelerated(firstCpfp); + } else if (cpfpInfo.acceleratedAt) { // Acceleration was cancelled: reset acceleration state + this.tx.acceleratedBy = cpfpInfo.acceleratedBy; + this.tx.acceleratedAt = cpfpInfo.acceleratedAt; + this.tx.feeDelta = cpfpInfo.feeDelta; + this.accelerationCanceled = true; + this.setIsAccelerated(firstCpfp); + } + + if (this.notAcceleratedOnLoad === null) { + this.notAcceleratedOnLoad = !this.isAcceleration; } if (!this.isAcceleration && this.fragmentParams.has('accelerate')) { @@ -830,12 +905,17 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.sigops = this.cpfpInfo.sigops; this.adjustedVsize = this.cpfpInfo.adjustedVsize; } - this.hasCpfp =!!(this.cpfpInfo && (this.cpfpInfo.bestDescendant || this.cpfpInfo.descendants?.length || this.cpfpInfo.ancestors?.length)); - this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01)); + this.hasCpfp =!!(this.cpfpInfo && relatives.length); + this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && this.tx.effectiveFeePerVsize !== (this.tx.fee / (this.tx.weight / 4)) && this.tx.effectiveFeePerVsize !== (this.tx.fee / Math.ceil(this.tx.weight / 4))); } setIsAccelerated(initialState: boolean = false) { - this.isAcceleration = ((this.tx.acceleration && (!this.tx.status.confirmed || this.waitingForAccelerationInfo)) || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id)))); + this.isAcceleration = + ( + (this.tx.acceleration && (!this.tx.status.confirmed || this.waitingForAccelerationInfo)) || + (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id))) + ) && + !this.accelerationCanceled; if (this.isAcceleration) { if (initialState) { this.accelerationFlowCompleted = true; @@ -843,25 +923,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } if (this.isAcceleration) { // this immediately returns cached stats if we fetched them recently - this.miningService.getMiningStats('1w').subscribe(stats => { + this.miningService.getMiningStats('1m').subscribe(stats => { 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); } @@ -871,7 +936,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit'); this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot'); this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf'); - this.tx.flags = getTransactionFlags(this.tx); + this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network); this.filters = this.tx.flags ? toFilters(this.tx.flags).filter(f => f.txPage) : []; this.checkAccelerationEligibility(); } else { @@ -902,6 +967,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { return false; } break; + case 'testnet4': + if (blockHeight < this.stateService.env.TESTNET4_BLOCK_AUDIT_START_HEIGHT) { + return false; + } + break; case 'signet': if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) { return false; @@ -915,6 +985,34 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { return true; } + isFirstSeenAvailable(blockHeight: number): boolean { + if (this.stateService.env.BASE_MODULE !== 'mempool') { + return false; + } + switch (this.stateService.network) { + case 'testnet': + if (this.stateService.env.TESTNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.TESTNET_TX_FIRST_SEEN_START_HEIGHT) { + return true; + } + break; + case 'testnet4': + if (this.stateService.env.TESTNET4_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.TESTNET4_TX_FIRST_SEEN_START_HEIGHT) { + return true; + } + break; + case 'signet': + if (this.stateService.env.SIGNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.SIGNET_TX_FIRST_SEEN_START_HEIGHT) { + return true; + } + break; + default: + if (this.stateService.env.MAINNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.MAINNET_TX_FIRST_SEEN_START_HEIGHT) { + return true; + } + } + return false; + } + resetTransaction() { this.firstLoad = false; this.gotInitialPosition = false; @@ -936,6 +1034,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; @@ -1053,6 +1152,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 @@ - + + + +
Timestamp - ‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }} -
- () -
-
Confirmed
First seen
First seen
Features - -
Audit - - @if (auditStatus.coinbase) { - Coinbase - } @else if (auditStatus.expected) { - Expected in Block - } @else if (auditStatus.seen) { - Seen in Mempool - } @else if (!auditStatus.conflict) { - Not seen in Mempool - } - @if (auditStatus.added) { - Added - } - @if (auditStatus.prioritized) { - Prioritized - } - @if (auditStatus.conflict) { - Conflict - } - -
ETA - - @if (eta.blocks >= 7) { - - Not any time soon - @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) { - Accelerate - } - - } @else if (network === 'liquid' || network === 'liquidtestnet') { - - } @else { - - - @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) { - Accelerate - } - - - - } - - - - -
- - - @if (isAcceleration) { - Accelerated - } - - {{ filter.label }} - -
Fee{{ tx.fee | number }} sat
Fee rate - - @if (tx?.status?.confirmed && tx.fee && !hasEffectiveFeeRate && !accelerationInfo) { -   - - } -
Accelerated fee rateEffective fee rate -
- @if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize || tx.acceleration)) { - - } @else { - - } - - @if (tx?.status?.confirmed && !tx.acceleration && !accelerationInfo && tx.fee && tx.effectiveFeePerVsize) { - - } -
- @if (hasCpfp) { - - } -
- -
Miner - - - {{ pool.name }} - - - -
@@ -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..c5c35dce5 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; @@ -78,7 +78,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { { index: 0, name: $localize`:@@dfc3c34e182ea73c5d784ff7c8135f087992dac1:All`, mode: 'and', filters: [], gradient: 'age' }, { index: 1, name: $localize`Consolidation`, mode: 'and', filters: ['consolidation'], gradient: 'fee' }, { index: 2, name: $localize`Coinjoin`, mode: 'and', filters: ['coinjoin'], gradient: 'fee' }, - { index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'], gradient: 'fee' }, + { index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'fake_scripthash', 'op_return'], gradient: 'fee' }, ]; goggleFlags = 0n; goggleMode: FilterMode = 'and'; 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..1f83cabc9 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 } ]`, }, @@ -9336,7 +9339,7 @@ export const restApiDocsData = [ fragment: "accelerator-history", title: "GET Acceleration History", description: { - default: "

Returns the user's past acceleration requests.

Pass one of the following for :status: all, requested, accelerating, mined, completed, failed. Pass true in :details to get a detailed history of the acceleration request.

" + default: "

Returns the user's past acceleration requests.

Pass one of the following for :status (required): all, requested, accelerating, mined, completed, failed.
Pass true in :details to get a detailed history of the acceleration request.

" }, urlString: "/v1/services/accelerator/history?status=:status&details=:details", showConditions: [""], @@ -9446,6 +9449,36 @@ export const restApiDocsData = [ } } }, + { + options: { officialOnly: true }, + type: "endpoint", + category: "accelerator-private", + httpRequestMethod: "POST", + fragment: "accelerator-cancel", + title: "POST Cancel Acceleration (Pro)", + description: { + default: "

Sends a request to cancel an acceleration in the accelerating status.
You can retreive eligible acceleration id using the history endpoint GET /api/v1/services/accelerator/history?status=accelerating." + }, + urlString: "/v1/services/accelerator/cancel", + showConditions: [""], + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `%{1}" "[[hostname]][[baseNetworkUrl]]/api/v1/services/accelerator/cancel`, //custom interpolation technique handled in replaceCurlPlaceholder() + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: ["id=42"], + headers: "X-Mempool-Auth: stacksats", + response: `HTTP/1.1 200 OK`, + }, + } + } + }, ]; export const faqData = [ 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 1ffc807a9..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; @@ -17,11 +17,13 @@ export interface Transaction { feePerVsize?: number; effectiveFeePerVsize?: number; ancestors?: Ancestor[]; + descendants?: Ancestor[]; bestDescendant?: BestDescendant | null; cpfpChecked?: boolean; acceleration?: boolean; acceleratedBy?: number[]; acceleratedAt?: number; + feeDelta?: number; deleteAfter?: number; _unblinded?: any; _deduced?: boolean; @@ -72,6 +74,8 @@ export interface Vin { issuance?: Issuance; // Custom lazy?: boolean; + // Ord + isInscription?: boolean; } interface Issuance { @@ -96,6 +100,8 @@ export interface Vout { valuecommitment?: number; asset?: string; pegout?: Pegout; + // Ord + isRunestone?: boolean; } interface Pegout { @@ -232,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 bda103691..b39f8e0d3 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -1,4 +1,4 @@ -import { AddressTxSummary, Block, Transaction } from "./electrs.interface"; +import { AddressTxSummary, Block, ChainStats, Transaction } from "./electrs.interface"; export interface OptimizedMempoolStats { added: number; @@ -31,6 +31,7 @@ export interface CpfpInfo { acceleration?: boolean; acceleratedBy?: number[]; acceleratedAt?: number; + feeDelta?: number; } export interface RbfInfo { @@ -142,6 +143,8 @@ export interface SinglePoolStats { rank: number; share: number; lastEstimatedHashrate: number; + lastEstimatedHashrate3d: number; + lastEstimatedHashrate1w: number; emptyBlockRatio: string; logo: string; slug: string; @@ -151,6 +154,8 @@ export interface SinglePoolStats { export interface PoolsStats { blockCount: number; lastEstimatedHashrate: number; + lastEstimatedHashrate3d: number; + lastEstimatedHashrate1w: number; pools: SinglePoolStats[]; } @@ -202,6 +207,7 @@ export interface BlockExtension { id: number; name: string; slug: string; + minerNames: string[] | null; } } @@ -210,6 +216,8 @@ export interface BlockExtended extends Block { } export interface BlockAudit extends BlockExtended { + version: number, + unseenTxs?: string[], missingTxs: string[], addedTxs: string[], prioritizedTxs: string[], @@ -236,7 +244,7 @@ export interface TransactionStripped { acc?: boolean; flags?: number | null; time?: number; - status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated'; + status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'added_deprioritized' | 'deprioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated'; context?: 'projected' | 'actual'; } @@ -250,6 +258,8 @@ export interface MempoolPosition { vsize: number, accelerated?: boolean, acceleratedBy?: number[], + acceleratedAt?: number, + feeDelta?: number, } export interface AccelerationPosition extends MempoolPosition { @@ -448,8 +458,26 @@ 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; +} export interface WalletAddress { address: string; active: boolean; - transactions?: AddressTxSummary[]; -} \ No newline at end of file + stats: ChainStats; + transactions: AddressTxSummary[]; +} diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index 6e3c0bb22..89c8e3884 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -1,13 +1,13 @@ 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'; block?: BlockExtended; blocks?: BlockExtended[]; - conversions?: any; + conversions?: Record; txConfirmed?: string; historicalDate?: string; mempoolInfo?: MempoolInfo; @@ -73,11 +73,13 @@ export interface MempoolBlockWithTransactions extends MempoolBlock { } export interface MempoolBlockDelta { + block: number; added: TransactionStripped[]; removed: string[]; changed: { txid: string, rate: number, flags: number, acc: boolean }[]; } export interface MempoolBlockState { + block: number; transactions: TransactionStripped[]; } export type MempoolBlockUpdate = MempoolBlockDelta | MempoolBlockState; @@ -142,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.scss b/frontend/src/app/lightning/channel/channel.component.scss index 9451c8112..89a43acf3 100644 --- a/frontend/src/app/lightning/channel/channel.component.scss +++ b/frontend/src/app/lightning/channel/channel.component.scss @@ -68,7 +68,7 @@ h3 { .loading-spinner { position: absolute; top: 400px; - z-index: 100; + z-index: 99; width: 100%; left: 0; @media (max-width: 767.98px) { 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 7f329eaf2..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', @@ -16,7 +16,7 @@ import { StateService } from '../../services/state.service'; position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; } `], }) 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 35cd8b236..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', @@ -19,7 +19,7 @@ import { StateService } from '../../services/state.service'; position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; } `], }) 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.scss b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.scss index 16482a0da..ad71cfb8e 100644 --- a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.scss +++ b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.scss @@ -102,7 +102,7 @@ position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; @media (max-width: 767.98px) { top: 550px; } @@ -110,7 +110,7 @@ .loading-spinner.widget { position: absolute; top: 200px; - z-index: 100; + z-index: 99; width: 100%; left: 0; @media (max-width: 767.98px) { @@ -120,7 +120,7 @@ .loading-spinner.nodepage { position: absolute; top: 200px; - z-index: 100; + z-index: 99; width: 100%; left: 0; } @@ -128,7 +128,7 @@ .loading-spinner.channelpage { position: absolute; top: 400px; - z-index: 100; + z-index: 99; width: 100%; left: 0; @media (max-width: 767.98px) { 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.scss b/frontend/src/app/lightning/nodes-channels/node-channels.component.scss index 78510203f..08dc5c2cb 100644 --- a/frontend/src/app/lightning/nodes-channels/node-channels.component.scss +++ b/frontend/src/app/lightning/nodes-channels/node-channels.component.scss @@ -8,7 +8,7 @@ left: 0; right: 0; width: 100%; - z-index: 100; + z-index: 99; } .spinner-border { 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.scss b/frontend/src/app/lightning/nodes-map/nodes-map.component.scss index 82362a257..d0fa1016f 100644 --- a/frontend/src/app/lightning/nodes-map/nodes-map.component.scss +++ b/frontend/src/app/lightning/nodes-map/nodes-map.component.scss @@ -68,7 +68,7 @@ position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; @media (max-width: 767.98px) { top: 550px; } 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 9784e0a5b..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', @@ -22,7 +22,7 @@ import { StateService } from '../../services/state.service'; position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; } `], changeDetection: ChangeDetectionStrategy.OnPush, 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.scss b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.scss index b9490d579..e1b66cc2b 100644 --- a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.scss +++ b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.scss @@ -46,7 +46,7 @@ position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; } .pool-distribution { 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 44f359ae8..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', @@ -22,7 +22,7 @@ import { StateService } from '../../services/state.service'; position: absolute; top: 50%; left: calc(50% - 15px); - z-index: 100; + z-index: 99; } `], }) 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 new file mode 100644 index 000000000..780e997db --- /dev/null +++ b/frontend/src/app/route-guards.ts @@ -0,0 +1,23 @@ +import { Injectable, inject } from '@angular/core'; +import { CanMatchFn, Route, Router, UrlSegment } from '@angular/router'; +import { NavigationService } from '@app/services/navigation.service'; + +@Injectable({ + providedIn: 'root' +}) +class GuardService { + constructor( + private router: Router, + private navigationService: NavigationService, + ) {} + + trackerGuard(route: Route, segments: UrlSegment[]): boolean { + const preferredRoute = this.router.getCurrentNavigation()?.extractedUrl.queryParams?.mode; + 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); +}; diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index ef0c87570..3c8cf8807 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, WalletAddress } 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' @@ -18,6 +18,7 @@ export class ApiService { private apiBasePath: string; // network path is /testnet, etc. or '' for mainnet private requestCache = new Map, expiry: number }>; + public blockSummaryLoaded: { [hash: string]: boolean } = {}; public blockAuditLoaded: { [hash: string]: boolean } = {}; constructor( @@ -244,6 +245,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'); } @@ -305,9 +319,14 @@ export class ApiService { } getStrippedBlockTransactions$(hash: string): Observable { + this.setBlockSummaryLoaded(hash); return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/summary'); } + getStrippedBlockTransaction$(hash: string, txid: string): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/tx/' + txid + '/summary'); + } + getDifficultyAdjustments$(interval: string | undefined): Observable { return this.httpClient.get( this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty-adjustments` + @@ -554,4 +573,12 @@ export class ApiService { getBlockAuditLoaded(hash) { return this.blockAuditLoaded[hash]; } + + async setBlockSummaryLoaded(hash: string) { + this.blockSummaryLoaded[hash] = true; + } + + getBlockSummaryLoaded(hash) { + return this.blockSummaryLoaded[hash]; + } } 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 f9549cc8a..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'; @@ -30,7 +30,6 @@ export class EnterpriseService { this.fetchSubdomainInfo(); this.disableSubnetworks(); this.stateService.env.ACCELERATOR = false; - this.stateService.env.ACCELERATOR_BUTTON = false; } else { this.insertMatomo(); } 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 7bbf6b759..760ce93cb 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; @@ -13,6 +13,8 @@ export interface MiningUnits { export interface MiningStats { lastEstimatedHashrate: number; + lastEstimatedHashrate3d: number; + lastEstimatedHashrate1w: number; blockCount: number; totalEmptyBlock: number; totalEmptyBlockRatio: string; @@ -31,12 +33,17 @@ export class MiningService { data: MiningStats; } } = {}; + poolsData: SinglePoolStats[] = []; constructor( private stateService: StateService, private apiService: ApiService, private storageService: StorageService, - ) { } + ) { + this.stateService.networkChanged$.subscribe((network) => { + this.clearCache(); + }); + } /** * Generate pool ranking stats @@ -57,7 +64,19 @@ export class MiningService { ); } } - + + /** + * Get names and slugs of all pools + */ + public getPools(): Observable { + return this.poolsData.length ? of(this.poolsData) : this.apiService.listPools$(undefined).pipe( + map(response => { + this.poolsData = response.body; + return this.poolsData; + }) + ); + + } /** * Set the hashrate power of ten we want to display */ @@ -112,6 +131,8 @@ export class MiningService { return { share: parseFloat((poolStat.blockCount / stats.blockCount * 100).toFixed(2)), lastEstimatedHashrate: poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate / hashrateDivider, + lastEstimatedHashrate3d: poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate3d / hashrateDivider, + lastEstimatedHashrate1w: poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate1w / hashrateDivider, emptyBlockRatio: (poolStat.emptyBlocks / poolStat.blockCount * 100).toFixed(2), logo: `/resources/mining-pools/` + poolStat.slug + '.svg', ...poolStat @@ -120,6 +141,8 @@ export class MiningService { return { lastEstimatedHashrate: stats.lastEstimatedHashrate / hashrateDivider, + lastEstimatedHashrate3d: stats.lastEstimatedHashrate3d / hashrateDivider, + lastEstimatedHashrate1w: stats.lastEstimatedHashrate1w / hashrateDivider, blockCount: stats.blockCount, totalEmptyBlock: totalEmptyBlock, totalEmptyBlockRatio: totalEmptyBlockRatio, @@ -128,4 +151,9 @@ export class MiningService { totalBlockCount: parseInt(response.headers.get('x-total-count'), 10), }; } + + private clearCache(): void { + this.cache = {}; + this.poolsData = []; + } } diff --git a/frontend/src/app/services/navigation.service.ts b/frontend/src/app/services/navigation.service.ts index 57f7f84dd..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' @@ -27,6 +27,7 @@ export class NavigationService { } }; networks = Object.keys(this.networkModules); + initialLoad = true; constructor( private stateService: StateService, @@ -40,6 +41,10 @@ export class NavigationService { if (this.enforceSubnetRestrictions(state)) { this.updateSubnetPaths(state); } + if (this.initialLoad) { + this.initialLoad = false; + } + this.updateSubnetPaths(state); }); } @@ -98,4 +103,8 @@ export class NavigationService { }); this.subnetPaths.next(subnetPaths); } + + isInitialLoad(): boolean { + return this.initialLoad; + } } 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 c26075198..2ecfe06ff 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; @@ -118,6 +117,9 @@ export class ServicesApiServices { } getJWT$() { + if (!this.stateService.env.OFFICIAL_MEMPOOL_SPACE) { + return of(null); + } return this.httpClient.get(`${this.stateService.env.SERVICES_API}/auth/getJWT`); } @@ -129,16 +131,20 @@ export class ServicesApiServices { return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/estimate`, { txInput: txInput }, { observe: 'response' }); } - accelerate$(txInput: string, userBid: number, accelerationUUID: string) { - return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate`, { txInput: txInput, userBid: userBid, accelerationUUID: accelerationUUID }); + accelerate$(txInput: string, userBid: number) { + return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate`, { txInput: txInput, userBid: userBid}); } - 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, userApprovedUSD: number) { + return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, 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, userApprovedUSD: number) { + return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD }); + } + + accelerateWithGooglePay$(txInput: string, token: string, cardTag: string, referenceId: string, userApprovedUSD: number) { + return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD }); } getAccelerations$(): Observable { @@ -153,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 ecc67fc34..2feb266d1 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 { 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 { 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, shareReplay } from 'rxjs/operators'; -import { StorageService } from './storage.service'; -import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils'; -import { ActiveFilter } from '../shared/filters.utils'; +import { filter, map, scan, share, shareReplay } from 'rxjs/operators'; +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; @@ -68,7 +68,12 @@ export interface Env { AUDIT: boolean; MAINNET_BLOCK_AUDIT_START_HEIGHT: number; TESTNET_BLOCK_AUDIT_START_HEIGHT: number; + TESTNET4_BLOCK_AUDIT_START_HEIGHT: number; SIGNET_BLOCK_AUDIT_START_HEIGHT: number; + MAINNET_TX_FIRST_SEEN_START_HEIGHT: number; + TESTNET_TX_FIRST_SEEN_START_HEIGHT: number; + TESTNET4_TX_FIRST_SEEN_START_HEIGHT: number; + SIGNET_TX_FIRST_SEEN_START_HEIGHT: number; HISTORICAL_PRICE: boolean; ACCELERATOR: boolean; ACCELERATOR_BUTTON: boolean; @@ -78,6 +83,7 @@ export interface Env { PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string; SERVICES_API?: string; customize?: Customization; + PROD_DOMAINS: string[]; } const defaultEnv: Env = { @@ -106,13 +112,19 @@ const defaultEnv: Env = { 'AUDIT': false, 'MAINNET_BLOCK_AUDIT_START_HEIGHT': 0, 'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0, + 'TESTNET4_BLOCK_AUDIT_START_HEIGHT': 0, 'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0, + 'MAINNET_TX_FIRST_SEEN_START_HEIGHT': 0, + 'TESTNET_TX_FIRST_SEEN_START_HEIGHT': 0, + 'TESTNET4_TX_FIRST_SEEN_START_HEIGHT': 0, + 'SIGNET_TX_FIRST_SEEN_START_HEIGHT': 0, 'HISTORICAL_PRICE': true, 'ACCELERATOR': false, 'ACCELERATOR_BUTTON': true, 'PUBLIC_ACCELERATIONS': false, 'ADDITIONAL_CURRENCIES': false, 'SERVICES_API': 'https://mempool.space/api/v1/services', + 'PROD_DOMAINS': [], }; @Injectable({ @@ -131,6 +143,7 @@ export class StateService { latestBlockHeight = -1; blocks: BlockExtended[] = []; mempoolSequence: number; + mempoolBlockState: { block: number, transactions: { [txid: string]: TransactionStripped} }; backend$ = new BehaviorSubject<'esplora' | 'electrum' | 'none'>('esplora'); networkChanged$ = new ReplaySubject(1); @@ -138,12 +151,12 @@ export class StateService { blocksSubject$ = new BehaviorSubject([]); blocks$: Observable; transactions$ = new BehaviorSubject(null); - conversions$ = new ReplaySubject(1); + conversions$ = new ReplaySubject>(1); bsqPrice$ = new ReplaySubject(1); mempoolInfo$ = new ReplaySubject(1); mempoolBlocks$ = new ReplaySubject(1); mempoolBlockUpdate$ = new Subject(); - liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>; + liveMempoolBlockTransactions$: Observable<{ block: number, transactions: { [txid: string]: TransactionStripped} }>; accelerations$ = new Subject(); liveAccelerations$: Observable; txConfirmed$ = new Subject<[string, BlockExtended]>(); @@ -158,7 +171,7 @@ export class StateService { mempoolRemovedTransactions$ = new Subject(); multiAddressTransactions$ = new Subject<{ [address: string]: { mempool: Transaction[], confirmed: Transaction[], removed: Transaction[] }}>(); blockTransactions$ = new Subject(); - walletTransactions$ = new Subject>(); + walletTransactions$ = new Subject(); isLoadingWebSocket$ = new ReplaySubject(1); isLoadingMempool$ = new BehaviorSubject(true); vbytesPerSecond$ = new ReplaySubject(1); @@ -205,12 +218,20 @@ 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') { this.env.MINING_DASHBOARD = false; } + if (document.location.hostname.endsWith('.onion')) { + this.env.SERVICES_API = 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1/services'; + } + if (this.isBrowser) { this.setNetworkBasedonUrl(window.location.pathname); this.setLightningBasedonUrl(window.location.pathname); @@ -228,33 +249,40 @@ export class StateService { } }); - if (this.referrer === 'https://cash.app/' && window.innerWidth < 850 && window.location.pathname.startsWith('/tx/')) { - this.router.navigate(['/tracker/' + window.location.pathname.slice(4)]); - } - - this.liveMempoolBlockTransactions$ = this.mempoolBlockUpdate$.pipe(scan((transactions: { [txid: string]: TransactionStripped }, change: MempoolBlockUpdate): { [txid: string]: TransactionStripped } => { + this.liveMempoolBlockTransactions$ = this.mempoolBlockUpdate$.pipe(scan((acc: { block: number, transactions: { [txid: string]: TransactionStripped } }, change: MempoolBlockUpdate): { block: number, transactions: { [txid: string]: TransactionStripped } } => { if (isMempoolState(change)) { const txMap = {}; change.transactions.forEach(tx => { txMap[tx.txid] = tx; }); - return txMap; + this.mempoolBlockState = { + block: change.block, + transactions: txMap + }; + return this.mempoolBlockState; } else { change.added.forEach(tx => { - transactions[tx.txid] = tx; + acc.transactions[tx.txid] = tx; }); change.removed.forEach(txid => { - delete transactions[txid]; + delete acc.transactions[txid]; }); change.changed.forEach(tx => { - if (transactions[tx.txid]) { - transactions[tx.txid].rate = tx.rate; - transactions[tx.txid].acc = tx.acc; + if (acc.transactions[tx.txid]) { + acc.transactions[tx.txid].rate = tx.rate; + acc.transactions[tx.txid].acc = tx.acc; } }); - return transactions; + this.mempoolBlockState = { + block: change.block, + transactions: acc.transactions + }; + return this.mempoolBlockState; } - }, {})); + }, {}), + share() + ); + this.liveMempoolBlockTransactions$.subscribe(); // Emits the full list of pending accelerations each time it changes this.liveAccelerations$ = this.accelerations$.pipe( 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 ba163bdd2..0f5368244 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; @@ -37,6 +37,8 @@ export class WebsocketService { private isTrackingWallet: boolean = false; private trackingWalletName: string; private trackingMempoolBlock: number; + private trackingMempoolBlockNetwork: string; + private stoppingTrackMempoolBlock: any | null = null; private latestGitCommit = ''; private onlineCheckTimeout: number; private onlineCheckTimeoutTwo: number; @@ -220,19 +222,32 @@ export class WebsocketService { this.websocketSubject.next({ 'track-asset': 'stop' }); } - startTrackMempoolBlock(block: number, force: boolean = false) { + startTrackMempoolBlock(block: number, force: boolean = false): boolean { + if (this.stoppingTrackMempoolBlock) { + clearTimeout(this.stoppingTrackMempoolBlock); + } // skip duplicate tracking requests - if (force || this.trackingMempoolBlock !== block) { + if (force || this.trackingMempoolBlock !== block || this.network !== this.trackingMempoolBlockNetwork) { this.websocketSubject.next({ 'track-mempool-block': block }); this.isTrackingMempoolBlock = true; this.trackingMempoolBlock = block; + this.trackingMempoolBlockNetwork = this.network; + return true; } + return false; } - stopTrackMempoolBlock() { - this.websocketSubject.next({ 'track-mempool-block': -1 }); + stopTrackMempoolBlock(): void { + if (this.stoppingTrackMempoolBlock) { + clearTimeout(this.stoppingTrackMempoolBlock); + } this.isTrackingMempoolBlock = false; - this.trackingMempoolBlock = null; + this.stoppingTrackMempoolBlock = setTimeout(() => { + this.stoppingTrackMempoolBlock = null; + this.websocketSubject.next({ 'track-mempool-block': -1 }); + this.trackingMempoolBlock = null; + this.stateService.mempoolBlockState = null; + }, 2000); } startTrackRbf(mode: 'all' | 'fullRbf') { @@ -441,6 +456,7 @@ export class WebsocketService { if (response['projected-block-transactions'].blockTransactions) { this.stateService.mempoolSequence = response['projected-block-transactions'].sequence; this.stateService.mempoolBlockUpdate$.next({ + block: this.trackingMempoolBlock, transactions: response['projected-block-transactions'].blockTransactions.map(uncompressTx), }); } else if (response['projected-block-transactions'].delta) { @@ -449,7 +465,7 @@ export class WebsocketService { this.startTrackMempoolBlock(this.trackingMempoolBlock, true); } else { this.stateService.mempoolSequence = response['projected-block-transactions'].sequence; - this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(response['projected-block-transactions'].delta)); + this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(this.trackingMempoolBlock, response['projected-block-transactions'].delta)); } } } diff --git a/frontend/src/app/shared/address-utils.ts b/frontend/src/app/shared/address-utils.ts index 92646af14..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' @@ -17,6 +17,7 @@ export type AddressType = 'fee' | 'v0_p2wsh' | 'v1_p2tr' | 'confidential' + | 'anchor' | 'unknown' const ADDRESS_PREFIXES = { @@ -188,6 +189,12 @@ export class AddressTypeInfo { const v = vin[0]; this.processScript(new ScriptInfo('scriptpubkey', v.prevout.scriptpubkey, v.prevout.scriptpubkey_asm)); } + } else if (this.type === 'unknown') { + for (const v of vin) { + if (v.prevout?.scriptpubkey === '51024e73') { + this.type = 'anchor'; + } + } } // and there's nothing more to learn from processing inputs for other types } @@ -197,6 +204,10 @@ export class AddressTypeInfo { if (!this.scripts.size) { this.processScript(new ScriptInfo('scriptpubkey', output.scriptpubkey, output.scriptpubkey_asm)); } + } else if (this.type === 'unknown') { + if (output.scriptpubkey === '51024e73') { + this.type = 'anchor'; + } } } @@ -206,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 697b11b5e..9b53600c1 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); @@ -170,8 +173,9 @@ export function uncompressTx(tx: TransactionCompressed): TransactionStripped { }; } -export function uncompressDeltaChange(delta: MempoolBlockDeltaCompressed): MempoolBlockDelta { +export function uncompressDeltaChange(block: number, delta: MempoolBlockDeltaCompressed): MempoolBlockDelta { return { + block, added: delta.added.map(uncompressTx), removed: delta.removed, changed: delta.changed.map(tx => ({ @@ -183,17 +187,54 @@ export function uncompressDeltaChange(delta: MempoolBlockDeltaCompressed): Mempo }; } -export function insecureRandomUUID(): string { - const hexDigits = '0123456789abcdef'; - const uuidLengths = [8, 4, 4, 4, 12]; - let uuid = ''; - for (const length of uuidLengths) { - for (let i = 0; i < length; i++) { - uuid += hexDigits[Math.floor(Math.random() * 16)]; - } - uuid += '-'; +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; } - return uuid.slice(0, -1); + 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 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 diff --git a/frontend/src/app/shared/components/address-type/address-type.component.html b/frontend/src/app/shared/components/address-type/address-type.component.html index fe4286689..598c21a6e 100644 --- a/frontend/src/app/shared/components/address-type/address-type.component.html +++ b/frontend/src/app/shared/components/address-type/address-type.component.html @@ -20,6 +20,9 @@ @case ('multisig') { bare multisig } + @case ('anchor') { + anchor + } @case (null) { unknown } 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 a14232bc5..e327a11de 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 + + }

}
@@ -79,7 +93,7 @@

Networks

Mainnet Explorer

Testnet3 Explorer

-

Testnet4 Explorer beta

+

Testnet4 Explorer

Signet Explorer

Liquid Testnet Explorer

Liquid 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..5f8c9f566 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; @@ -281,13 +303,17 @@ footer .nowrap { margin: 0 auto; } + .enterprise-logo { + max-width: 100%; + } + footer .site-options { float: none; margin-top: 15px; } } -@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.html b/frontend/src/app/shared/components/testnet-alert/testnet-alert.component.html index 8aedc4947..7e87e312b 100644 --- a/frontend/src/app/shared/components/testnet-alert/testnet-alert.component.html +++ b/frontend/src/app/shared/components/testnet-alert/testnet-alert.component.html @@ -5,9 +5,6 @@ @if (stateService.network === 'testnet') { Testnet3 is deprecated, and will soon be replaced by Testnet4 } - @if (stateService.network === 'testnet4') { - Testnet4 is not yet finalized, and may be reset at anytime. - }