Merge branch 'master' into orangesurf/trademark-updates-2

This commit is contained in:
wiz
2024-02-22 15:56:08 +09:00
committed by GitHub
323 changed files with 32614 additions and 12318 deletions

7
frontend/.gitignore vendored
View File

@@ -6,6 +6,13 @@
/out-tsc
server.run.js
# docker
Dockerfile
entrypoint.sh
nginx-mempool.conf
nginx.conf
wait-for
# Only exists if Bazel was run
/bazel-out

View File

@@ -58,8 +58,8 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.6.0",
"cypress-fail-on-console-error": "~5.0.0",
"cypress": "^13.6.2",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",
"start-server-and-test": "~2.0.0"
@@ -4068,9 +4068,9 @@
}
},
"node_modules/@sinonjs/fake-timers": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
"integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
"version": "11.2.2",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz",
"integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==",
"optional": true,
"dependencies": {
"@sinonjs/commons": "^3.0.0"
@@ -6242,17 +6242,18 @@
"optional": true
},
"node_modules/chai": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz",
"integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.4.0.tgz",
"integrity": "sha512-x9cHNq1uvkCdU+5xTkNh5WtgD4e4yDFCsp9jVc7N7qVeKeftv3gO/ZrviX5d+3ZfxdYnZXZYujjRInu1RogU6A==",
"optional": true,
"dependencies": {
"assertion-error": "^1.1.0",
"check-error": "^1.0.2",
"deep-eql": "^3.0.1",
"get-func-name": "^2.0.0",
"check-error": "^1.0.3",
"deep-eql": "^4.1.3",
"get-func-name": "^2.0.2",
"loupe": "^2.3.6",
"pathval": "^1.1.1",
"type-detect": "^4.0.5"
"type-detect": "^4.0.8"
},
"engines": {
"node": ">=4"
@@ -6277,10 +6278,13 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
},
"node_modules/check-error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
"integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
"optional": true,
"dependencies": {
"get-func-name": "^2.0.2"
},
"engines": {
"node": "*"
}
@@ -7079,9 +7083,9 @@
"peer": true
},
"node_modules/cypress": {
"version": "13.6.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz",
"integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==",
"version": "13.6.2",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.2.tgz",
"integrity": "sha512-TW3bGdPU4BrfvMQYv1z3oMqj71YI4AlgJgnrycicmPZAXtvywVFZW9DAToshO65D97rCWfG/kqMFsYB6Kp91gQ==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
@@ -7137,13 +7141,13 @@
}
},
"node_modules/cypress-fail-on-console-error": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cypress-fail-on-console-error/-/cypress-fail-on-console-error-5.0.0.tgz",
"integrity": "sha512-xui/aSu8rmExZjZNgId3iX0MsGZih6ZoFH+54vNHrK3HaqIZZX5hUuNhAcmfSoM1rIDc2DeITeVaMn/hiQ9IWQ==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cypress-fail-on-console-error/-/cypress-fail-on-console-error-5.1.0.tgz",
"integrity": "sha512-u/AXLE9obLd9KcGHkGJluJVZeOj1EEOFOs0URxxca4FrftUDJQ3u+IoNfjRUjsrBKmJxgM4vKd0G10D+ZT1uIA==",
"optional": true,
"dependencies": {
"chai": "^4.3.4",
"sinon": "^15.0.0",
"chai": "^4.3.10",
"sinon": "^17.0.0",
"sinon-chai": "^3.7.0",
"type-detect": "^4.0.8"
}
@@ -7403,15 +7407,15 @@
"integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw="
},
"node_modules/deep-eql": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
"integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
"integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==",
"optional": true,
"dependencies": {
"type-detect": "^4.0.0"
},
"engines": {
"node": ">=0.12"
"node": ">=6"
}
},
"node_modules/deep-equal": {
@@ -9268,9 +9272,9 @@
"devOptional": true
},
"node_modules/follow-redirects": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"funding": [
{
"type": "individual",
@@ -11759,6 +11763,15 @@
"node": ">=8.0"
}
},
"node_modules/loupe": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
"integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
"optional": true,
"dependencies": {
"get-func-name": "^2.0.1"
}
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -12537,9 +12550,9 @@
}
},
"node_modules/nise": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz",
"integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==",
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz",
"integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==",
"optional": true,
"dependencies": {
"@sinonjs/commons": "^2.0.0",
@@ -12558,6 +12571,24 @@
"type-detect": "4.0.8"
}
},
"node_modules/nise/node_modules/@sinonjs/fake-timers": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
"integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
"optional": true,
"dependencies": {
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/nise/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
"integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
"optional": true,
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/nise/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
@@ -14842,16 +14873,16 @@
]
},
"node_modules/sinon": {
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz",
"integrity": "sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==",
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz",
"integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==",
"optional": true,
"dependencies": {
"@sinonjs/commons": "^3.0.0",
"@sinonjs/fake-timers": "^10.3.0",
"@sinonjs/fake-timers": "^11.2.2",
"@sinonjs/samsam": "^8.0.0",
"diff": "^5.1.0",
"nise": "^5.1.4",
"nise": "^5.1.5",
"supports-color": "^7.2.0"
},
"funding": {
@@ -19882,9 +19913,9 @@
}
},
"@sinonjs/fake-timers": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
"integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
"version": "11.2.2",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz",
"integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==",
"optional": true,
"requires": {
"@sinonjs/commons": "^3.0.0"
@@ -21594,17 +21625,18 @@
"optional": true
},
"chai": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz",
"integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.4.0.tgz",
"integrity": "sha512-x9cHNq1uvkCdU+5xTkNh5WtgD4e4yDFCsp9jVc7N7qVeKeftv3gO/ZrviX5d+3ZfxdYnZXZYujjRInu1RogU6A==",
"optional": true,
"requires": {
"assertion-error": "^1.1.0",
"check-error": "^1.0.2",
"deep-eql": "^3.0.1",
"get-func-name": "^2.0.0",
"check-error": "^1.0.3",
"deep-eql": "^4.1.3",
"get-func-name": "^2.0.2",
"loupe": "^2.3.6",
"pathval": "^1.1.1",
"type-detect": "^4.0.5"
"type-detect": "^4.0.8"
}
},
"chalk": {
@@ -21623,10 +21655,13 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
},
"check-error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
"optional": true
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
"integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
"optional": true,
"requires": {
"get-func-name": "^2.0.2"
}
},
"check-more-types": {
"version": "2.24.0",
@@ -22237,9 +22272,9 @@
"peer": true
},
"cypress": {
"version": "13.6.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz",
"integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==",
"version": "13.6.2",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.2.tgz",
"integrity": "sha512-TW3bGdPU4BrfvMQYv1z3oMqj71YI4AlgJgnrycicmPZAXtvywVFZW9DAToshO65D97rCWfG/kqMFsYB6Kp91gQ==",
"optional": true,
"requires": {
"@cypress/request": "^3.0.0",
@@ -22403,13 +22438,13 @@
}
},
"cypress-fail-on-console-error": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cypress-fail-on-console-error/-/cypress-fail-on-console-error-5.0.0.tgz",
"integrity": "sha512-xui/aSu8rmExZjZNgId3iX0MsGZih6ZoFH+54vNHrK3HaqIZZX5hUuNhAcmfSoM1rIDc2DeITeVaMn/hiQ9IWQ==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cypress-fail-on-console-error/-/cypress-fail-on-console-error-5.1.0.tgz",
"integrity": "sha512-u/AXLE9obLd9KcGHkGJluJVZeOj1EEOFOs0URxxca4FrftUDJQ3u+IoNfjRUjsrBKmJxgM4vKd0G10D+ZT1uIA==",
"optional": true,
"requires": {
"chai": "^4.3.4",
"sinon": "^15.0.0",
"chai": "^4.3.10",
"sinon": "^17.0.0",
"sinon-chai": "^3.7.0",
"type-detect": "^4.0.8"
}
@@ -22490,9 +22525,9 @@
"integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw="
},
"deep-eql": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
"integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
"integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==",
"optional": true,
"requires": {
"type-detect": "^4.0.0"
@@ -23957,9 +23992,9 @@
"devOptional": true
},
"follow-redirects": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q=="
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw=="
},
"foreach": {
"version": "2.0.5",
@@ -25754,6 +25789,15 @@
"streamroller": "^3.0.2"
}
},
"loupe": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
"integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
"optional": true,
"requires": {
"get-func-name": "^2.0.1"
}
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -26361,9 +26405,9 @@
}
},
"nise": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz",
"integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==",
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz",
"integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==",
"optional": true,
"requires": {
"@sinonjs/commons": "^2.0.0",
@@ -26382,6 +26426,26 @@
"type-detect": "4.0.8"
}
},
"@sinonjs/fake-timers": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
"integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
"optional": true,
"requires": {
"@sinonjs/commons": "^3.0.0"
},
"dependencies": {
"@sinonjs/commons": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
"integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
"optional": true,
"requires": {
"type-detect": "4.0.8"
}
}
}
},
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
@@ -28036,16 +28100,16 @@
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="
},
"sinon": {
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz",
"integrity": "sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==",
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz",
"integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==",
"optional": true,
"requires": {
"@sinonjs/commons": "^3.0.0",
"@sinonjs/fake-timers": "^10.3.0",
"@sinonjs/fake-timers": "^11.2.2",
"@sinonjs/samsam": "^8.0.0",
"diff": "^5.1.0",
"nise": "^5.1.4",
"nise": "^5.1.5",
"supports-color": "^7.2.0"
},
"dependencies": {

View File

@@ -110,8 +110,8 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.6.0",
"cypress-fail-on-console-error": "~5.0.0",
"cypress": "^13.6.2",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",
"start-server-and-test": "~2.0.0"

View File

@@ -22,6 +22,7 @@ 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';
const providers = [
ElectrsApiService,
@@ -40,6 +41,7 @@ const providers = [
FiatCurrencyPipe,
CapAddressPipe,
AppPreloadingStrategy,
ServicesApiServices,
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
];

View File

@@ -30,7 +30,7 @@ export class BisqDashboardComponent implements OnInit {
ngOnInit(): void {
this.seoService.setTitle($localize`:@@meta.title.bisq.markets:Markets`);
this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Source Project. See Bisq market prices, trading activity, and more.`);
this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See Bisq market prices, trading activity, and more.`);
this.websocketService.want(['blocks']);
this.volumes$ = this.bisqApiService.getAllVolumesDay$()

View File

@@ -1,16 +1,16 @@
<div id="become-sponsor-container">
<div id="become-sponsor-container" [ngClass]="context">
<div class="become-sponsor community">
<p style="font-weight: 700; font-size: 18px;">If you're an individual...</p>
<a href="https://mempool.space/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button" (click)="onSponsorClick($event)">Become a Community Sponsor</a>
<a [href]="host + '/sponsor'" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button" (click)="onSponsorClick($event)">Become a Community Sponsor</a>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Exclusive swag</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Your avatar on the About page</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> And more coming soon :)</p>
</div>
<div class="become-sponsor enterprise">
<p style="font-weight: 700; font-size: 18px;">If you're a business...</p>
<a href="https://mempool.space/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button" (click)="onEnterpriseClick($event)">Become an Enterprise Sponsor</a>
<a [href]="host + '/enterprise'" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button" (click)="onEnterpriseClick($event)">Become an Enterprise Sponsor</a>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Increased API limits</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Co-branded instance</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> 99% service-level agreement</p>
</div>
</div>
</div>

View File

@@ -6,6 +6,11 @@
align-items: center;
gap: 20px;
margin: 68px auto;
text-align: center;
}
#become-sponsor-container.account {
margin: 20px auto;
}
.become-sponsor {

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core';
import { Component, Input } from '@angular/core';
import { EnterpriseService } from '../../services/enterprise.service';
@Component({
@@ -7,6 +7,9 @@ import { EnterpriseService } from '../../services/enterprise.service';
styleUrls: ['./about-sponsors.component.scss'],
})
export class AboutSponsorsComponent {
@Input() host = 'https://mempool.space';
@Input() context = 'about';
constructor(private enterpriseService: EnterpriseService) {
}

View File

@@ -405,7 +405,7 @@
<div class="copyright">
<div class="title">
Copyright &copy; 2019-2023<br>
Copyright &copy; 2019-2024<br>
Mempool Space K.K.<br>
and other shadowy super-coders
</div>
@@ -422,7 +422,7 @@
Trademark Notice<br>
</div>
<p>
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&trade;, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool logo, the mempool Square logo, the mempool 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.
</p>
<p>
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on &lt;https://mempool.space/trademark-policy&gt;.

View File

@@ -25,7 +25,7 @@
></app-accelerate-fee-graph>
</ng-container>
<ng-container *ngIf="estimate">
<ng-container *ngIf="estimate else loadingEstimate">
<div [class]="{estimateDisabled: error}">
<div *ngIf="user && !estimate.hasAccess">
@@ -243,4 +243,9 @@
</div>
</ng-container>
</div>
</div>
<ng-template #loadingEstimate>
<div class="skeleton-loader"></div>
<br>
</ng-template>

View File

@@ -4,6 +4,8 @@ import { Subscription, catchError, of, tap } from 'rxjs';
import { StorageService } from '../../services/storage.service';
import { Transaction } from '../../interfaces/electrs.interface';
import { nextRoundNumber } from '../../shared/common.utils';
import { ServicesApiServices } from '../../services/services-api.service';
import { AudioService } from '../../services/audio.service';
export type AccelerationEstimate = {
txSummary: TxSummary;
@@ -61,8 +63,9 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
maxRateOptions: RateOption[] = [];
constructor(
private apiService: ApiService,
private servicesApiService: ServicesApiServices,
private storageService: StorageService,
private audioService: AudioService,
private cd: ChangeDetectorRef
) { }
@@ -81,7 +84,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
ngOnInit() {
this.user = this.storageService.getAuth()?.user ?? null;
this.estimateSubscription = this.apiService.estimate$(this.tx.txid).pipe(
this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe(
tap((response) => {
if (response.status === 204) {
this.estimate = undefined;
@@ -181,11 +184,12 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
if (this.accelerationSubscription) {
this.accelerationSubscription.unsubscribe();
}
this.accelerationSubscription = this.apiService.accelerate$(
this.accelerationSubscription = this.servicesApiService.accelerate$(
this.tx.txid,
this.userBid
).subscribe({
next: () => {
this.audioService.playSound('ascend-chime-cartoon');
this.showSuccess = true;
this.scrollToPreviewWithTimeout('successAlert', 'center');
this.estimateSubscription.unsubscribe();
@@ -210,4 +214,4 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
onResize(): void {
this.isMobile = window.innerWidth <= 767.98;
}
}
}

View File

@@ -27,12 +27,6 @@
</form>
</div>
<div *ngIf="widget">
<div class="item">
<h5 class="card-title" i18n="acceleration.block-fees">Out-of-band Fees Per Block</h5>
</div>
</div>
<div [class.chart]="!widget" [class.chart-widget]="widget" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>

View File

@@ -53,11 +53,6 @@
padding-bottom: 55px;
}
}
.chart-widget {
width: 100%;
height: 100%;
max-height: 290px;
}
h5 {
margin-bottom: 10px;

View File

@@ -1,8 +1,7 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnDestroy, OnInit } from '@angular/core';
import { EChartsOption, graphic } from 'echarts';
import { Observable, Subscription, combineLatest } from 'rxjs';
import { Observable, Subscription, combineLatest, fromEvent } from 'rxjs';
import { map, max, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../../services/api.service';
import { SeoService } from '../../../services/seo.service';
import { formatNumber } from '@angular/common';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
@@ -11,6 +10,8 @@ import { StorageService } from '../../../services/storage.service';
import { MiningService } from '../../../services/mining.service';
import { ActivatedRoute } from '@angular/router';
import { Acceleration } from '../../../interfaces/node-api.interface';
import { ServicesApiServices } from '../../../services/services-api.service';
import { ApiService } from '../../../services/api.service';
@Component({
selector: 'app-acceleration-fees-graph',
@@ -28,6 +29,7 @@ import { Acceleration } from '../../../interfaces/node-api.interface';
})
export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
@Input() widget: boolean = false;
@Input() height: number | string = '200';
@Input() right: number | string = 45;
@Input() left: number | string = 75;
@Input() accelerations$: Observable<Acceleration[]>;
@@ -54,6 +56,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
@Inject(LOCALE_ID) public locale: string,
private seoService: SeoService,
private apiService: ApiService,
private servicesApiService: ServicesApiServices,
private formBuilder: UntypedFormBuilder,
private storageService: StorageService,
private miningService: MiningService,
@@ -66,26 +69,27 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
this.seoService.setTitle($localize`:@@6c453b11fd7bd159ae30bc381f367bc736d86909:Acceleration Fees`);
this.isLoading = true;
if (this.widget) {
this.miningWindowPreference = '1m';
this.timespan = this.miningWindowPreference;
this.statsObservable$ = combineLatest([
(this.accelerations$ || this.apiService.getAccelerationHistory$({ timeframe: this.miningWindowPreference })),
(this.accelerations$ || this.servicesApiService.getAccelerationHistory$({ timeframe: this.miningWindowPreference })),
this.apiService.getHistoricalBlockFees$(this.miningWindowPreference),
fromEvent(window, 'resize').pipe(startWith(null)),
]).pipe(
tap(([accelerations, blockFeesResponse]) => {
this.prepareChartOptions(accelerations, blockFeesResponse.body);
}),
map(([accelerations, blockFeesResponse]) => {
return {
avgFeesPaid: accelerations.filter(acc => acc.status === 'completed').reduce((total, acc) => total + acc.feePaid, 0) / accelerations.length
avgFeesPaid: accelerations.filter(acc => acc.status === 'completed').reduce((total, acc) => total + (acc.feePaid - acc.baseFee - acc.vsizeFee), 0) / accelerations.length
};
}),
);
} else {
this.seoService.setTitle($localize`:@@bcf34abc2d9ed8f45a2f65dd464c46694e9a181e:Acceleration Fees`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('1w');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@@ -101,7 +105,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
this.isLoading = true;
this.storageService.setValue('miningWindowPreference', timespan);
this.timespan = timespan;
return this.apiService.getAccelerationHistory$({});
return this.servicesApiService.getAccelerationHistory$({});
})
),
this.radioGroupForm.get('dateSpan').valueChanges.pipe(
@@ -151,7 +155,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
while (last <= val.avgHeight) {
blockCount++;
totalFeeDelta += (blockAccelerations[last] || []).reduce((total, acc) => total + acc.feeDelta, 0);
totalFeePaid += (blockAccelerations[last] || []).reduce((total, acc) => total + acc.feePaid, 0);
totalFeePaid += (blockAccelerations[last] || []).reduce((total, acc) => total + (acc.feePaid - acc.baseFee - acc.vsizeFee), 0);
totalCount += (blockAccelerations[last] || []).length;
last++;
}
@@ -173,6 +177,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
],
animation: false,
grid: {
height: this.height,
right: this.right,
left: this.left,
bottom: this.widget ? 30 : 80,
@@ -246,7 +251,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
icon: 'roundRect',
},
{
name: 'Out-of-band fees per block',
name: 'Total bid boost per block',
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
@@ -256,7 +261,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
],
selected: {
'In-band fees per block': false,
'Out-of-band fees per block': true,
'Total bid boost per block': true,
},
show: !this.widget,
},
@@ -299,7 +304,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
{
legendHoverLink: false,
zlevel: 1,
name: 'Out-of-band fees per block',
name: 'Total bid boost per block',
data: data.map(block => [block.timestamp * 1000, block.avgFeePaid, block.avgHeight]),
stack: 'Total',
type: 'bar',

View File

@@ -1,14 +1,14 @@
<div class="stats-wrapper" *ngIf="accelerationStats$ | async as stats; else loading">
<div class="stats-container">
<div class="item">
<h5 class="card-title" i18n="address.transactions">Transactions</h5>
<h5 class="card-title" i18n="accelerator.requests">Requests</h5>
<div class="card-text">
<div>{{ stats.count }}</div>
<div class="symbol" i18n="accelerator.total-accelerated">accelerated</div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="accelerator.out-of-band-fees">Out-of-band Fees</h5>
<h5 class="card-title" i18n="accelerator.total-boost">Total Bid Boost</h5>
<div class="card-text">
<div>{{ stats.totalFeesPaid / 100_000_000 | amountShortener: 4 }} <span class="symbol" i18n="shared.btc|BTC">BTC</span></div>
<span class="fiat">
@@ -17,7 +17,7 @@
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="accelerator.success-rate">Success rate</h5>
<h5 class="card-title" i18n="accelerator.success-rate">Success Rate</h5>
<div class="card-text">
<div>{{ stats.successRate.toFixed(2) }} %</div>
<div class="symbol" i18n="accelerator.mined-next-block">mined</div>
@@ -29,21 +29,21 @@
<ng-template #loading>
<div class="stats-container loading-container">
<div class="item">
<h5 class="card-title" i18n="address.transactions">Transactions</h5>
<h5 class="card-title" i18n="accelerator.requests">Requests</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="accelerator.out-of-band-fees">Out-of-band Fees</h5>
<h5 class="card-title" i18n="accelerator.total-boost">Total Bid Boost</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="accelerator.success-rate">Success rate</h5>
<h5 class="card-title" i18n="accelerator.success-rate">Success Rate</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>

View File

@@ -27,11 +27,11 @@ export class AccelerationStatsComponent implements OnInit {
let totalFeesPaid = 0;
let totalSucceeded = 0;
let totalCanceled = 0;
for (const acceleration of accelerations) {
if (acceleration.status === 'completed') {
for (const acc of accelerations) {
if (acc.status === 'completed') {
totalSucceeded++;
totalFeesPaid += acceleration.feePaid || 0;
} else if (acceleration.status === 'failed') {
totalFeesPaid += (acc.feePaid - acc.baseFee - acc.vsizeFee) || 0;
} else if (acc.status === 'failed') {
totalCanceled++;
}
}

View File

@@ -1,10 +1,10 @@
<div class="container-xl" style="min-height: 335px" [class.widget]="widget" [class.full-height]="!widget">
<div class="container-xl widget-container" [class.widget]="widget" [class.full-height]="!widget">
<h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Accelerations</h1>
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
<div class="clearfix"></div>
<div style="min-height: 295px" *ngIf="accelerationList$ | async as accelerations">
<div class="acceleration-list" *ngIf="accelerationList$ | async as accelerations">
<table *ngIf="!accelerations || accelerations.length; else noData" class="table table-borderless table-fixed">
<thead>
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
@@ -14,7 +14,7 @@
<th class="time text-right" i18n="accelerator.block">Requested</th>
</ng-container>
<ng-container *ngIf="!pending">
<th class="fee text-right" i18n="transaction.fee|Transaction fee">Out-of-band Fee</th>
<th class="fee text-right" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
<th class="block text-right" i18n="accelerator.block">Block</th>
<th class="status text-right" i18n="transaction.status|Transaction Status">Status</th>
</ng-container>
@@ -39,7 +39,7 @@
</ng-container>
<ng-container *ngIf="!pending">
<td *ngIf="acceleration.feePaid" class="fee text-right">
{{ (acceleration.feePaid) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
{{ (acceleration.boost) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
</td>
<td *ngIf="!acceleration.feePaid" class="fee text-right">
~
@@ -48,7 +48,7 @@
<a [routerLink]="['/block' | relativeUrl, acceleration.blockHeight]">{{ acceleration.blockHeight }}</a>
</td>
<td class="status text-right">
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="transaction.rbf.mined">Pending</span>
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
<span *ngIf="acceleration.status === 'mined' || acceleration.status === 'completed'" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
<span *ngIf="acceleration.status === 'failed'" class="badge badge-danger" i18n="accelerator.canceled">Canceled</span>
</td>

View File

@@ -14,11 +14,24 @@
.container-xl.legacy {
max-width: 1140px;
}
.container-xl.widget-container {
min-height: 335px;
@media (max-width: 767px) {
min-height: auto;
}
}
.container {
max-width: 100%;
}
.acceleration-list {
min-height: 295px;
@media (max-width: 767px) {
min-height: auto;
}
}
tr, td, th {
border: 0px;
padding-top: 0.65rem !important;
@@ -50,37 +63,82 @@ tr, td, th {
}
.txid {
width: 25%;
@media (max-width: 1100px) {
padding-right: 10px;
}
@media (max-width: 875px) {
@media (max-width: 500px) {
display: none;
}
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 30%;
}
.fee {
width: 35%;
.fee, .block, .status {
width: 15%;
@media (max-width: 720px) {
width: 20%;
}
}
.block {
width: 20%;
}
.widget {
.txid {
width: 30%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 30%;
@media (max-width: 1060px) and (min-width: 768px) {
display: none;
}
@media (max-width: 500px) {
display: none;
}
}
.bid {
width: 30%;
}
.fee-rate {
width: 20%;
@media (max-width: 1060px) and (min-width: 768px) {
text-align: start !important;
}
@media (max-width: 500px) {
text-align: start !important;
}
@media (max-width: 840px) and (min-width: 768px) {
display: none;
}
@media (max-width: 410px) {
display: none;
}
}
.time {
width: 25%;
}
.bid {
width: 30%;
min-width: 150px;
@media (max-width: 840px) and (min-width: 768px) {
text-align: start !important;
}
@media (max-width: 410px) {
text-align: start !important;
}
}
.status {
width: 20%
.time {
width: 25%;
}
.fee {
width: 30%;
@media (max-width: 1060px) and (min-width: 768px) {
text-align: start !important;
}
@media (max-width: 500px) {
text-align: start !important;
}
}
.block {
width: 20%;
}
.status {
width: 20%
}
}
/* Tooltip text */
@@ -122,4 +180,7 @@ tr, td, th {
flex-direction: row;
align-items: center;
justify-content: center;
@media (max-width: 767px) {
height: 100px;
}
}

View File

@@ -1,9 +1,9 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core';
import { Observable, catchError, of, switchMap, tap } from 'rxjs';
import { Acceleration, 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 { ServicesApiServices } from '../../../services/services-api.service';
@Component({
selector: 'app-accelerations-list',
@@ -26,7 +26,7 @@ export class AccelerationsListComponent implements OnInit {
skeletonLines: number[] = [];
constructor(
private apiService: ApiService,
private servicesApiService: ServicesApiServices,
private websocketService: WebsocketService,
public stateService: StateService,
private cd: ChangeDetectorRef,
@@ -41,7 +41,7 @@ export class AccelerationsListComponent implements OnInit {
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
const accelerationObservable$ = this.accelerations$ || (this.pending ? this.apiService.getAccelerations$() : this.apiService.getAccelerationHistory$({ timeframe: '1m' }));
const accelerationObservable$ = this.accelerations$ || (this.pending ? this.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistory$({ timeframe: '1m' }));
this.accelerationList$ = accelerationObservable$.pipe(
switchMap(accelerations => {
if (this.pending) {
@@ -49,6 +49,9 @@ export class AccelerationsListComponent implements OnInit {
acceleration.status = acceleration.status || 'accelerating';
}
}
for (const acc of accelerations) {
acc.boost = acc.feePaid - acc.baseFee - acc.vsizeFee;
}
if (this.widget) {
return of(accelerations.slice(-6).reverse());
} else {

View File

@@ -7,7 +7,7 @@
<!-- pending stats -->
<div class="col">
<div class="main-title">
<span [attr.data-cy]="'pending-accelerations'" i18n="accelerator.pending-accelerations">Active accelerations</span>
<span [attr.data-cy]="'pending-accelerations'" i18n="accelerator.pending-accelerations">Active Accelerations</span>
</div>
<div class="card-wrapper">
<div class="card">
@@ -37,6 +37,11 @@
<div class="col" style="margin-bottom: 1.47rem">
<div class="card">
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
<a class="title-link" href="" [routerLink]="['/mempool-block/0' | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.mempool-goggles-accelerations">Mempool Goggles: Accelerations</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: #4a68b9"></fa-icon>
</a>
<div class="mempool-block-wrapper">
<app-mempool-block-overview [index]="0" [overrideColors]="getAcceleratorColor"></app-mempool-block-overview>
</div>
@@ -48,7 +53,15 @@
<div class="col" style="margin-bottom: 1.47rem">
<div class="card graph-card">
<div class="card-body pl-2 pr-2">
<app-acceleration-fees-graph [attr.data-cy]="'acceleration-fees'" [widget]=true [accelerations$]="accelerations$"></app-acceleration-fees-graph>
<h5 class="card-title" i18n="acceleration.total-bid-boost">Total Bid Boost</h5>
<div class="mempool-graph">
<app-acceleration-fees-graph
[height]="graphHeight"
[attr.data-cy]="'acceleration-fees'"
[widget]=true
[accelerations$]="accelerations$"
></app-acceleration-fees-graph>
</div>
<div class="mt-1"><a [attr.data-cy]="'acceleration-fees-view-more'" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
@@ -69,7 +82,7 @@
<div class="card list-card">
<div class="card-body">
<div class="title-link">
<h5 class="card-title d-inline" i18n="dashboard.recent-accelerations">Active Accelerations</h5>
<h5 class="card-title d-inline" i18n="accelerator.pending-accelerations">Active Accelerations</h5>
</div>
<app-accelerations-list [attr.data-cy]="'pending-accelerations'" [widget]=true [pending]="true" [accelerations$]="pendingAccelerations$"></app-accelerations-list>
</div>
@@ -80,7 +93,7 @@
<div class="col">
<div class="card list-card">
<div class="card-body">
<a class="title-link" href="" [routerLink]="['/acceleration-list' | relativeUrl]">
<a class="title-link" href="" [routerLink]="['/acceleration/list' | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.recent-accelerations">Recent Accelerations</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: #4a68b9"></fa-icon>

View File

@@ -17,6 +17,16 @@
}
}
.mempool-graph {
height: 295px;
@media (min-width: 768px) {
height: 325px;
}
@media (min-width: 992px) {
height: 409px;
}
}
.card-title {
font-size: 1rem;
color: #4a68b9;
@@ -135,14 +145,31 @@
}
.card {
height: 385px;
@media (min-width: 768px) {
height: 420px;
}
@media (min-width: 992px) {
height: 510px;
}
}
.list-card {
height: 410px;
@media (max-width: 767px) {
height: auto;
}
}
.mempool-block-wrapper {
max-height: 380px;
max-width: 380px;
max-height: 430px;
max-width: 430px;
margin: auto;
@media (min-width: 768px) {
max-height: 344px;
max-width: 344px;
}
@media (min-width: 992px) {
max-height: 430px;
max-width: 430px;
}
}

View File

@@ -1,14 +1,14 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, HostListener, OnInit } from '@angular/core';
import { SeoService } from '../../../services/seo.service';
import { WebsocketService } from '../../../services/websocket.service';
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service';
import { Observable, Subject, catchError, combineLatest, distinctUntilChanged, interval, map, of, share, startWith, switchMap, tap } from 'rxjs';
import { ApiService } from '../../../services/api.service';
import { Observable, catchError, combineLatest, distinctUntilChanged, interval, map, of, share, startWith, 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, mempoolFeeColors } from '../../../app.constants';
import { ServicesApiServices } from '../../../services/services-api.service';
const acceleratedColor: Color = hexToColor('8F5FF6');
const normalColors = mempoolFeeColors.map(hex => hexToColor(hex + '5F'));
@@ -30,43 +30,48 @@ export class AcceleratorDashboardComponent implements OnInit {
minedAccelerations$: Observable<Acceleration[]>;
loadingBlocks: boolean = true;
graphHeight: number = 300;
constructor(
private seoService: SeoService,
private websocketService: WebsocketService,
private apiService: ApiService,
private serviceApiServices: ServicesApiServices,
private stateService: StateService,
) {
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Accelerator Dashboard`);
}
ngOnInit(): void {
this.onResize();
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
this.pendingAccelerations$ = interval(30000).pipe(
startWith(true),
switchMap(() => {
return this.apiService.getAccelerations$();
}),
catchError((e) => {
return of([]);
return this.serviceApiServices.getAccelerations$().pipe(
catchError(() => {
return of([]);
}),
);
}),
share(),
);
this.accelerations$ = this.stateService.chainTip$.pipe(
distinctUntilChanged(),
switchMap((chainTip) => {
return this.apiService.getAccelerationHistory$({ timeframe: '1m' });
}),
catchError((e) => {
return of([]);
switchMap(() => {
return this.serviceApiServices.getAccelerationHistory$({ timeframe: '1m' }).pipe(
catchError(() => {
return of([]);
}),
);
}),
share(),
);
this.minedAccelerations$ = this.accelerations$.pipe(
map(accelerations => {
return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status))
return accelerations.filter(acc => ['mined', 'completed', 'failed'].includes(acc.status));
})
);
@@ -119,4 +124,15 @@ export class AcceleratorDashboardComponent implements OnInit {
return normalColors[feeLevelIndex] || normalColors[mempoolFeeColors.length - 1];
}
}
@HostListener('window:resize', ['$event'])
onResize(): void {
if (window.innerWidth >= 992) {
this.graphHeight = 330;
} else if (window.innerWidth >= 768) {
this.graphHeight = 245;
} else {
this.graphHeight = 210;
}
}
}

View File

@@ -1,10 +1,10 @@
<div class="stats-wrapper" *ngIf="accelerationStats$ | async as stats; else loading">
<div class="stats-container">
<div class="item">
<h5 class="card-title" i18n="address.transactions">Transactions</h5>
<h5 class="card-title" i18n="accelerator.requests">Requests</h5>
<div class="card-text">
<div>{{ stats.count }}</div>
<div class="symbol" i18n="accelerator.total-accelerated">accelerated</div>
<div class="symbol" i18n="accelerator.total-pending">pending</div>
</div>
</div>
<div class="item">
@@ -17,7 +17,7 @@
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="accelerator.total-vsize">Total vsize</h5>
<h5 class="card-title" i18n="accelerator.total-vsize">Total Vsize</h5>
<div class="card-text">
<div [innerHTML]="'&lrm;' + (stats.totalVsize * 4 | vbytes: 2)"></div>
<div class="symbol">{{ (stats.totalVsize / 1_000_000 * 100).toFixed(2) }}% <span i18n="accelerator.percent-of-next-block"> of next block</span></div>
@@ -29,7 +29,7 @@
<ng-template #loading>
<div class="stats-container loading-container">
<div class="item">
<h5 class="card-title" i18n="address.transactions">Transactions</h5>
<h5 class="card-title" i18n="accelerator.requests">Requests</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
@@ -43,7 +43,7 @@
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="accelerator.total-vsize">Total vsize</h5>
<h5 class="card-title" i18n="accelerator.total-vsize">Total Vsize</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>

View File

@@ -1,8 +1,8 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { ApiService } from '../../../services/api.service';
import { Acceleration } from '../../../interfaces/node-api.interface';
import { ServicesApiServices } from '../../../services/services-api.service';
@Component({
selector: 'app-pending-stats',
@@ -15,11 +15,11 @@ export class PendingStatsComponent implements OnInit {
public accelerationStats$: Observable<any>;
constructor(
private apiService: ApiService,
private servicesApiService: ServicesApiServices,
) { }
ngOnInit(): void {
this.accelerationStats$ = (this.accelerations$ || this.apiService.getAccelerations$()).pipe(
this.accelerationStats$ = (this.accelerations$ || this.servicesApiService.getAccelerations$()).pipe(
switchMap(accelerations => {
let totalAccelerations = 0;
let totalFeeDelta = 0;

View File

@@ -43,7 +43,7 @@ export class AddressLabelsComponent implements OnChanges {
handleVin() {
if (this.vin.inner_witnessscript_asm) {
if (this.vin.inner_witnessscript_asm.indexOf('OP_DEPTH OP_PUSHNUM_12 OP_EQUAL OP_IF OP_PUSHNUM_11') === 0) {
if (this.vin.inner_witnessscript_asm.indexOf('OP_DEPTH OP_PUSHNUM_12 OP_EQUAL OP_IF OP_PUSHNUM_11') === 0 || this.vin.inner_witnessscript_asm.indexOf('OP_PUSHNUM_15 OP_CHECKMULTISIG OP_IFDUP OP_NOTIF OP_PUSHBYTES_2') === 1259) {
if (this.vin.witness.length > 11) {
this.label = 'Liquid Peg Out';
} else {

View File

@@ -31,8 +31,7 @@ export class AddressComponent implements OnInit, OnDestroy {
addressLoadingStatus$: Observable<number>;
addressInfo: null | AddressInformation = null;
totalConfirmedTxCount = 0;
loadedConfirmedTxCount = 0;
fullyLoaded = false;
txCount = 0;
received = 0;
sent = 0;
@@ -66,7 +65,7 @@ export class AddressComponent implements OnInit, OnDestroy {
switchMap((params: ParamMap) => {
this.error = undefined;
this.isLoadingAddress = true;
this.loadedConfirmedTxCount = 0;
this.fullyLoaded = false;
this.address = null;
this.isLoadingTransactions = true;
this.transactions = null;
@@ -105,7 +104,7 @@ export class AddressComponent implements OnInit, OnDestroy {
.pipe(
filter((address) => !!address),
tap((address: Address) => {
if ((this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') && /^([m-zA-HJ-NP-Z1-9]{26,35}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[a-km-zA-HJ-NP-Z1-9]{80})$/.test(address.address)) {
if ((this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') && /^([a-zA-HJ-NP-Z1-9]{26,35}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[a-km-zA-HJ-NP-Z1-9]{80})$/.test(address.address)) {
this.apiService.validateAddress$(address.address)
.subscribe((addressInfo) => {
this.addressInfo = addressInfo;
@@ -128,7 +127,6 @@ export class AddressComponent implements OnInit, OnDestroy {
this.tempTransactions = transactions;
if (transactions.length) {
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
this.loadedConfirmedTxCount += transactions.filter((tx) => tx.status.confirmed).length;
}
const fetchTxs: string[] = [];
@@ -191,8 +189,6 @@ export class AddressComponent implements OnInit, OnDestroy {
this.audioService.playSound('magic');
}
}
this.totalConfirmedTxCount++;
this.loadedConfirmedTxCount++;
});
}
@@ -252,16 +248,19 @@ export class AddressComponent implements OnInit, OnDestroy {
}
loadMore() {
if (this.isLoadingTransactions || !this.totalConfirmedTxCount || this.loadedConfirmedTxCount >= this.totalConfirmedTxCount) {
if (this.isLoadingTransactions || this.fullyLoaded) {
return;
}
this.isLoadingTransactions = true;
this.retryLoadMore = false;
this.electrsApiService.getAddressTransactions$(this.address.address, this.lastTransactionTxId)
.subscribe((transactions: Transaction[]) => {
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
this.loadedConfirmedTxCount += transactions.length;
this.transactions = this.transactions.concat(transactions);
if (transactions && transactions.length) {
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
this.transactions = this.transactions.concat(transactions);
} else {
this.fullyLoaded = true;
}
this.isLoadingTransactions = false;
},
(error) => {
@@ -278,7 +277,6 @@ export class AddressComponent implements OnInit, OnDestroy {
this.received = this.address.chain_stats.funded_txo_sum + this.address.mempool_stats.funded_txo_sum;
this.sent = this.address.chain_stats.spent_txo_sum + this.address.mempool_stats.spent_txo_sum;
this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count;
this.totalConfirmedTxCount = this.address.chain_stats.tx_count;
}
ngOnDestroy() {

View File

@@ -19,7 +19,7 @@
</ng-template>
<ng-template #default>
&lrm;{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis / 100000000 | number : digitsInfo }}
<span class="symbol"><ng-template [ngIf]="network === 'liquid'">L-</ng-template>
<span class="symbol"><ng-template [ngIf]="network === 'liquid' && !forceBtc">L-</ng-template>
<ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template>
<ng-template [ngIf]="network === 'testnet'">t</ng-template>
<ng-template [ngIf]="network === 'signet'">s</ng-template>BTC</span>

View File

@@ -23,6 +23,7 @@ export class AmountComponent implements OnInit, OnDestroy {
@Input() noFiat = false;
@Input() addPlus = false;
@Input() blockConversion: Price;
@Input() forceBtc: boolean = false;
constructor(
private stateService: StateService,

View File

@@ -4,7 +4,7 @@
<ng-template #title>
<div class="main-title">
<h2>{{ group.name }}</h2>
<h2>{{ group['group'].name }}</h2>
<div class="sub-title" i18n>Group of {{ group.assets.length | number }} assets</div>
</div>
</ng-template>

View File

@@ -1,7 +1,11 @@
<div class="block-filters" [class.filters-active]="activeFilters.length > 0" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200">
<div class="block-filters" [class.filters-active]="activeFilters.length > 0" [class.any-mode]="filterMode === 'or'" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200">
<a *ngIf="menuOpen" [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-do-mempool-goggles-work" class="info-badges" i18n-ngbTooltip="Mempool Goggles tooltip" ngbTooltip="select filter categories to highlight matching transactions">
<span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon>
</a>
<div class="filter-bar">
<button class="menu-toggle" (click)="menuOpen = !menuOpen">
<fa-icon [icon]="['fas', 'filter']"></fa-icon>
<button class="menu-toggle" (click)="menuOpen = !menuOpen" title="Mempool Goggles">
<app-svg-images name="goggles" width="100%" height="100%"></app-svg-images>
</button>
<div class="active-tags">
<ng-container *ngFor="let filter of activeFilters;">
@@ -10,11 +14,20 @@
</div>
</div>
<div class="filter-menu" *ngIf="menuOpen && cssWidth > 280">
<h5>Match</h5>
<div class="btn-group btn-group-toggle">
<label class="btn btn-xs blue mode-toggle" [class.active]="filterMode === 'and'">
<input type="radio" [value]="'all'" fragment="all" (click)="setFilterMode('and')">All
</label>
<label class="btn btn-xs green mode-toggle" [class.active]="filterMode === 'or'">
<input type="radio" [value]="'any'" fragment="any" (click)="setFilterMode('or')">Any
</label>
</div>
<ng-container *ngFor="let group of filterGroups;">
<h5>{{ group.label }}</h5>
<div class="filter-group">
<ng-container *ngFor="let filter of group.filters;">
<button class="btn filter-tag" [class.selected]="filterFlags[filter.key]" (click)="toggleFilter(filter.key)">{{ filter.label }}</button>
<button *ngIf="!disabledFilters[filter.key]" class="btn filter-tag" [class.selected]="filterFlags[filter.key]" (click)="toggleFilter(filter.key)">{{ filter.label }}</button>
</ng-container>
</div>
</ng-container>

View File

@@ -20,7 +20,21 @@
margin-left: 0.5em;
}
.info-badges {
display: flex;
flex-direction: row;
align-items: center;
float: right;
&:hover, &:active {
text-decoration: none;
}
}
.menu-toggle {
width: 2em;
height: 2em;
padding: 0px 1px;
opacity: 0;
cursor: pointer;
color: white;
@@ -63,6 +77,49 @@
}
}
&.any-mode {
.filter-tag {
border: solid 1px #1a9436;
&.selected {
background-color: #1a9436;
}
}
}
.btn-group {
font-size: 0.9em;
margin-right: 0.25em;
}
.mode-toggle {
padding: 0.2em 0.5em;
pointer-events: all;
line-height: 1.5;
background: #181b2daf;
&:first-child {
border-top-left-radius: 0.2rem;
border-bottom-left-radius: 0.2rem;
}
&:last-child {
border-top-right-radius: 0.2rem;
border-bottom-right-radius: 0.2rem;
}
&.blue {
border: solid 1px #105fb0;
&.active {
background: #105fb0;
}
}
&.green {
border: solid 1px #1a9436;
&.active {
background: #1a9436;
}
}
}
:host-context(.block-overview-graph:hover) &, &:hover, &:active {
.menu-toggle {
opacity: 0.5;
@@ -118,6 +175,11 @@
.filter-tag {
font-size: 0.7em;
}
.mode-toggle {
font-size: 0.7em;
margin-bottom: 5px;
margin-top: 2px;
}
}
&.tiny {

View File

@@ -1,5 +1,7 @@
import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core';
import { FilterGroups, TransactionFilters } from '../../shared/filters.utils';
import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core';
import { ActiveFilter, FilterGroups, FilterMode, TransactionFilters } from '../../shared/filters.utils';
import { StateService } from '../../services/state.service';
import { Subscription } from 'rxjs';
@Component({
@@ -7,24 +9,56 @@ import { FilterGroups, TransactionFilters } from '../../shared/filters.utils';
templateUrl: './block-filters.component.html',
styleUrls: ['./block-filters.component.scss'],
})
export class BlockFiltersComponent implements OnChanges {
export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
@Input() cssWidth: number = 800;
@Output() onFilterChanged: EventEmitter<bigint | null> = new EventEmitter();
@Input() excludeFilters: string[] = [];
@Output() onFilterChanged: EventEmitter<ActiveFilter | null> = new EventEmitter();
filterSubscription: Subscription;
filters = TransactionFilters;
filterGroups = FilterGroups;
disabledFilters: { [key: string]: boolean } = {};
activeFilters: string[] = [];
filterFlags: { [key: string]: boolean } = {};
filterMode: FilterMode = 'and';
menuOpen: boolean = false;
constructor(
private stateService: StateService,
private cd: ChangeDetectorRef,
) {}
ngOnInit(): void {
this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => {
this.filterMode = active.mode;
for (const key of Object.keys(this.filterFlags)) {
this.filterFlags[key] = false;
}
for (const key of active.filters) {
this.filterFlags[key] = !this.disabledFilters[key];
}
this.activeFilters = [...active.filters.filter(key => !this.disabledFilters[key])];
this.onFilterChanged.emit({ mode: active.mode, filters: this.activeFilters });
});
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.cssWidth) {
this.cd.markForCheck();
}
if (changes.excludeFilters) {
this.disabledFilters = {};
this.excludeFilters.forEach(filter => {
this.disabledFilters[filter] = true;
});
}
}
setFilterMode(mode): void {
this.filterMode = mode;
this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters });
this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] });
}
toggleFilter(key): void {
@@ -46,7 +80,9 @@ export class BlockFiltersComponent implements OnChanges {
// remove active filter
this.activeFilters = this.activeFilters.filter(f => f != key);
}
this.onFilterChanged.emit(this.getBooleanFlags());
const booleanFlags = this.getBooleanFlags();
this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters });
this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] });
}
getBooleanFlags(): bigint | null {
@@ -62,9 +98,13 @@ export class BlockFiltersComponent implements OnChanges {
@HostListener('document:click', ['$event'])
onClick(event): boolean {
// click away from menu
if (!event.target.closest('button')) {
if (!event.target.closest('button') && !event.target.closest('label')) {
this.menuOpen = false;
}
return true;
}
ngOnDestroy(): void {
this.filterSubscription.unsubscribe();
}
}

View File

@@ -13,6 +13,9 @@
[auditEnabled]="auditHighlighting"
[blockConversion]="blockConversion"
></app-block-overview-tooltip>
<app-block-filters *ngIf="showFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
<app-block-filters *ngIf="webGlEnabled && showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
<div *ngIf="!webGlEnabled" class="placeholder">
<span i18n="webgl-disabled">Your browser does not support this feature.</span>
</div>
</div>
</div>

View File

@@ -7,6 +7,19 @@
justify-content: center;
align-items: center;
grid-column: 1/-1;
.placeholder {
display: flex;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
height: 100%;
width: 100%;
align-items: center;
justify-content: center;
}
}
.grid-align {

View File

@@ -9,6 +9,8 @@ import { Price } from '../../services/price.service';
import { StateService } from '../../services/state.service';
import { Subscription } from 'rxjs';
import { defaultColorFunction, setOpacity, defaultFeeColors, defaultAuditFeeColors, defaultMarginalFeeColors, defaultAuditColors } from './utils';
import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils';
import { detectWebGL } from '../../shared/graphs.utils';
const unmatchedOpacity = 0.2;
const unmatchedFeeColors = defaultFeeColors.map(c => setOpacity(c, unmatchedOpacity));
@@ -40,7 +42,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() unavailable: boolean = false;
@Input() auditHighlighting: boolean = false;
@Input() showFilters: boolean = false;
@Input() excludeFilters: string[] = [];
@Input() filterFlags: bigint | null = null;
@Input() filterMode: FilterMode = 'and';
@Input() blockConversion: Price;
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
@@ -71,12 +75,17 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
searchText: string;
searchSubscription: Subscription;
filtersAvailable: boolean = true;
activeFilterFlags: bigint | null = null;
webGlEnabled = true;
constructor(
readonly ngZone: NgZone,
readonly elRef: ElementRef,
private stateService: StateService,
) {
this.webGlEnabled = detectWebGL();
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
this.searchSubscription = this.stateService.searchText$.subscribe((text) => {
this.searchText = text;
@@ -110,17 +119,22 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
if (changes.overrideColor && this.scene) {
this.scene.setColorFunction(this.overrideColors);
}
if ((changes.filterFlags || changes.showFilters) && this.scene) {
this.setFilterFlags(this.filterFlags);
if ((changes.filterFlags || changes.showFilters || changes.filterMode)) {
this.setFilterFlags();
}
}
setFilterFlags(flags: bigint | null): void {
if (flags != null) {
this.scene.setColorFunction(this.getFilterColorFunction(flags));
} else {
this.scene.setColorFunction(this.overrideColors);
setFilterFlags(goggle?: ActiveFilter): void {
this.filterMode = goggle?.mode || this.filterMode;
this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags;
if (this.scene) {
if (this.activeFilterFlags != null && this.filtersAvailable) {
this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags));
} else {
this.scene.setColorFunction(this.overrideColors);
}
}
this.start();
}
ngOnDestroy(): void {
@@ -149,6 +163,11 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
// initialize the scene without any entry transition
setup(transactions: TransactionStripped[]): void {
const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
if (filtersAvailable !== this.filtersAvailable) {
this.setFilterFlags();
}
this.filtersAvailable = filtersAvailable;
if (this.scene) {
this.scene.setup(transactions);
this.readyNextFrame = true;
@@ -259,7 +278,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset,
colorFunction: this.overrideColors });
colorFunction: this.getColorFunction() });
this.start();
}
}
@@ -491,11 +510,13 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
onTxClick(cssX: number, cssY: number, keyModifier: boolean = false) {
const x = cssX * window.devicePixelRatio;
const y = cssY * window.devicePixelRatio;
const selected = this.scene.getTxAt({ x, y });
if (selected && selected.txid) {
this.txClickEvent.emit({ tx: selected, keyModifier });
if (this.scene) {
const x = cssX * window.devicePixelRatio;
const y = cssY * window.devicePixelRatio;
const selected = this.scene.getTxAt({ x, y });
if (selected && selected.txid) {
this.txClickEvent.emit({ tx: selected, keyModifier });
}
}
}
@@ -503,9 +524,19 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.txHoverEvent.emit(hoverId);
}
getColorFunction(): ((tx: TxView) => Color) {
if (this.filterFlags) {
return this.getFilterColorFunction(this.filterFlags);
} else if (this.activeFilterFlags) {
return this.getFilterColorFunction(this.activeFilterFlags);
} else {
return this.overrideColors;
}
}
getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) {
return (tx: TxView) => {
if ((tx.bigintFlags & flags) === flags) {
if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) {
return defaultColorFunction(tx);
} else {
return defaultColorFunction(

View File

@@ -11,7 +11,7 @@ export default class BlockScene {
getColor: ((tx: TxView) => Color) = defaultColorFunction;
orientation: string;
flip: boolean;
animationDuration: number = 1000;
animationDuration: number = 900;
configAnimationOffset: number | null;
animationOffset: number;
highlightingEnabled: boolean;

View File

@@ -58,6 +58,10 @@
<td *ngSwitchCase="'accelerated'"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></td>
</ng-container>
</tr>
<tr *ngIf="!auditEnabled && tx && tx.status === 'accelerated'">
<td class="td-width"></td>
<td><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></td>
</tr>
</tbody>
</table>
</div>

View File

@@ -11,6 +11,7 @@
text-align: left;
min-width: 320px;
pointer-events: none;
z-index: 11;
&.clickable {
pointer-events: all;

View File

@@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/co
import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { switchMap, tap, throttleTime, catchError, shareReplay, startWith, pairwise, filter } from 'rxjs/operators';
import { of, Subscription, asyncScheduler } from 'rxjs';
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';
@@ -10,6 +10,7 @@ import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.in
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';
@Component({
selector: 'app-block-preview',
@@ -42,7 +43,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
public stateService: StateService,
private seoService: SeoService,
private openGraphService: OpenGraphService,
private apiService: ApiService
private apiService: ApiService,
private servicesApiService: ServicesApiServices,
) { }
ngOnInit() {
@@ -121,21 +123,37 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
this.overviewSubscription = block$.pipe(
startWith(null),
pairwise(),
switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id)
.pipe(
catchError((err) => {
this.overviewError = err;
this.openGraphService.fail('block-viz-' + this.rawId);
return of([]);
}),
switchMap((transactions) => {
return of({ transactions, direction: 'down' });
})
)
switchMap(([prevBlock, block]) => {
return forkJoin([
this.apiService.getStrippedBlockTransactions$(block.id)
.pipe(
catchError((err) => {
this.overviewError = err;
this.openGraphService.fail('block-viz-' + this.rawId);
return of([]);
}),
switchMap((transactions) => {
return of(transactions);
})
),
this.stateService.env.ACCELERATOR === true && block.height > 819500 ? this.servicesApiService.getAccelerationHistory$({ blockHash: block.id }) : of([])
]);
}
),
)
.subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
.subscribe(([transactions, accelerations]) => {
this.strippedTransactions = transactions;
const acceleratedInBlock = {};
for (const acc of accelerations) {
acceleratedInBlock[acc.txid] = acc;
}
for (const tx of transactions) {
if (acceleratedInBlock[tx.txid]) {
tx.acc = true;
}
}
this.isLoadingOverview = false;
if (this.blockGraph) {
this.blockGraph.destroy();

View File

@@ -42,12 +42,12 @@
<ng-container *ngIf="!isLoadingBlock; else skeletonRows">
<tr>
<td class="td-width" i18n="block.hash">Hash</td>
<td>&lrm;<a [routerLink]="['/block/' | relativeUrl, block.id]" title="{{ block.id }}">{{ block.id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.id"></app-clipboard></td>
<td>&lrm;<a [routerLink]="['/block/' | relativeUrl, block.id]" title="{{ block.id }}">{{ block.id | shortenString : 13 }}</a> <app-clipboard [text]="block.id"></app-clipboard></td>
</tr>
<tr>
<td i18n="block.timestamp">Timestamp</td>
<td>
<app-timestamp [unixTime]="block.timestamp" [precision]="1" minUnit="minute"></app-timestamp>
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="block.timestamp" [precision]="1" minUnit="minute"></app-timestamp>
</td>
</tr>
<tr>
@@ -59,7 +59,7 @@
<td [innerHTML]="'&lrm;' + (block.weight | wuBytes: 2)"></td>
</tr>
<tr *ngIf="auditAvailable">
<td><ng-container i18n="latest-blocks.health">Health</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="what-is-block-health"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></td>
<td><ng-container i18n="latest-blocks.health">Health</ng-container><a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="what-is-block-health"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></td>
<td>
<span
class="health-badge badge"
@@ -115,6 +115,8 @@
[orientation]="'top'"
[flip]="false"
[blockConversion]="blockConversion"
[showFilters]="true"
[excludeFilters]="['replacement']"
(txClickEvent)="onTxClick($event)"
></app-block-overview-graph>
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
@@ -229,23 +231,29 @@
<div class="block-graph-wrapper">
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="86"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"></app-block-overview-graph>
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"
[showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph>
<ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container>
</div>
<ng-container *ngIf="network !== 'liquid'">
<ng-container *ngTemplateOutlet="isMobile && mode === 'actual' ? actualDetails : expectedDetails"></ng-container>
<ng-template [ngIf]="!isLoadingOverview" [ngIfElse]="loadingDetailsSkeletons">
<ng-container *ngTemplateOutlet="isMobile && mode === 'actual' ? actualDetails : expectedDetails"></ng-container>
</ng-template>
</ng-container>
</div>
<div class="col-sm" *ngIf="!isMobile">
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container><a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
<div class="block-graph-wrapper">
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="86"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" [auditHighlighting]="showAudit"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"></app-block-overview-graph>
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"
[showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph>
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
</div>
<ng-container *ngIf="network !== 'liquid'">
<ng-container *ngTemplateOutlet="actualDetails"></ng-container>
<ng-template [ngIf]="!isLoadingOverview" [ngIfElse]="loadingDetailsSkeletons">
<ng-container *ngTemplateOutlet="actualDetails"></ng-container>
</ng-template>
</ng-container>
</div>
</div>
@@ -452,5 +460,24 @@
</table>
</ng-template>
<ng-template #loadingDetailsSkeletons>
<table class="table table-borderless table-striped audit-details-table">
<tbody>
<tr>
<td class="w-50" i18n="block.total-fees|Total fees in a block">Total fees</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n="block.weight">Weight</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n="mempool-block.transactions">Transactions</td>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</ng-template>
<br>
<br>

View File

@@ -57,11 +57,6 @@
text-align: left;
}
}
.info-link {
color: rgba(255, 255, 255, 0.4);
margin-left: 5px;
}
.difference {
margin-left: 0.5em;

View File

@@ -16,6 +16,7 @@ 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';
@Component({
selector: 'app-block',
@@ -103,6 +104,7 @@ export class BlockComponent implements OnInit, OnDestroy {
private apiService: ApiService,
private priceService: PriceService,
private cacheService: CacheService,
private servicesApiService: ServicesApiServices,
) {
this.webGlEnabled = detectWebGL();
}
@@ -328,17 +330,28 @@ export class BlockComponent implements OnInit, OnDestroy {
this.overviewError = err;
return of(null);
})
)
),
this.stateService.env.ACCELERATOR === true && block.height > 819500 ? this.servicesApiService.getAccelerationHistory$({ blockHash: block.id }) : of([])
]);
})
)
.subscribe(([transactions, blockAudit]) => {
.subscribe(([transactions, blockAudit, accelerations]) => {
if (transactions) {
this.strippedTransactions = transactions;
} else {
this.strippedTransactions = [];
}
const acceleratedInBlock = {};
for (const acc of accelerations) {
acceleratedInBlock[acc.txid] = acc;
}
for (const tx of transactions) {
if (acceleratedInBlock[tx.txid]) {
tx.acc = true;
}
}
this.blockAudit = null;
if (transactions && blockAudit) {
const inTemplate = {};

View File

@@ -26,7 +26,7 @@
</div>
<ng-template #emptyfees>
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
&nbsp;
<app-fee-rate unitClass=""></app-fee-rate>
</div>
</ng-template>
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-fee-span'" class="fee-span"
@@ -37,7 +37,7 @@
</div>
<ng-template #emptyfeespan>
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fee-span">
&nbsp;
<app-fee-rate unitClass=""></app-fee-rate>
</div>
</ng-template>
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-total-fees'" *ngIf="showMiningInfo"

View File

@@ -46,7 +46,7 @@
</div>
</td>
<td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
</td>
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
<a
@@ -92,21 +92,18 @@
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td *ngIf="isMempoolModule" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
<span class="skeleton-loader" style="max-width: 125px"></span>
<span class="skeleton-loader" style="max-width: 150px"></span>
</td>
<td class="timestamp" *ngIf="!widget" [class]="isMempoolModule ? '' : 'legacy'">
<span class="skeleton-loader" style="max-width: 150px"></span>
</td>
<td class="mined" *ngIf="!widget" [class]="isMempoolModule ? '' : 'legacy'">
<span class="skeleton-loader" style="max-width: 125px"></span>
</td>
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td *ngIf="isMempoolModule" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td *ngIf="isMempoolModule && !widget" class="fees text-right" [class]="isMempoolModule ? '' : 'legacy'">
<td *ngIf="isMempoolModule && !auditAvailable || isMempoolModule && !widget" class="fees text-right" [class]="isMempoolModule ? '' : 'legacy'">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td *ngIf="auditAvailable && !widget" class="fee-delta" [class]="isMempoolModule ? '' : 'legacy'">

View File

@@ -14,7 +14,7 @@
</div>
<div class="item">
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
<div class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span>
@@ -24,9 +24,6 @@
{{ epochData.change | absolute | number: '1.2-2' }}
<span class="symbol">%</span>
</div>
<ng-template #recentlyAdjusted>
<div class="card-text">&#8212;</div>
</ng-template>
<div class="symbol">
<span i18n="difficulty-box.previous">Previous</span>:
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
@@ -47,20 +44,30 @@
</div>
</div>
<div class="item" *ngIf="showHalving">
<h5 class="card-title" i18n="difficulty-box.next-halving" i18n-ngbTooltip="difficulty-box.next-halving"
ngbTooltip="Next Halving" placement="bottom" #averagefee [disableTooltip]="!isEllipsisActive(averagefee)">Next Halving</h5>
<div class="card-text">
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
<h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
<div class="card-text" i18n-ngbTooltip="mining.average-fee" [ngbTooltip]="halvingBlocksLeft" [tooltipContext]="{ epochData: epochData }" placement="bottom">
<span>{{ timeUntilHalving | date }}</span>
<div class="symbol" *ngIf="blocksUntilHalving === 1; else approxTime">
<app-time kind="until" [time]="epochData.adjustedTimeAvg + now" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
</div>
<ng-template #approxTime>
<div class="symbol">
<app-time kind="until" [time]="timeUntilHalving" [fastRender]="false" [fixedRender]="true" [precision]="0" [numUnits]="2" [units]="['year', 'day', 'hour', 'minute']"></app-time>
</div>
</ng-template>
</div>
</div>
<div class="symbol"><app-time kind="until" [time]="epochData.timeUntilHalving" [fastRender]="true" [precision]="1"></app-time></div>
</div>
</div>
</div>
</div>
</div>
<ng-template #halvingBlocksLeft let-epochData="epochData">
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
</ng-template>
<ng-template #loadingDifficulty>
<div class="difficulty-skeleton loading-container">
<div class="item">

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { combineLatest, Observable, timer } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { StateService } from '../../services/state.service';
interface EpochProgress {
@@ -15,6 +15,8 @@ interface EpochProgress {
previousRetarget: number;
blocksUntilHalving: number;
timeUntilHalving: number;
timeAvg: number;
adjustedTimeAvg: number;
}
@Component({
@@ -26,6 +28,9 @@ interface EpochProgress {
export class DifficultyMiningComponent implements OnInit {
isLoadingWebSocket$: Observable<boolean>;
difficultyEpoch$: Observable<EpochProgress>;
blocksUntilHalving: number | null = null;
timeUntilHalving = 0;
now = new Date().getTime();
@Input() showProgress = true;
@Input() showHalving = false;
@@ -64,8 +69,9 @@ export class DifficultyMiningComponent implements OnInit {
colorPreviousAdjustments = '#ffffff66';
}
const blocksUntilHalving = 210000 - (maxHeight % 210000);
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
this.blocksUntilHalving = 210000 - (maxHeight % 210000);
this.timeUntilHalving = new Date().getTime() + (this.blocksUntilHalving * 600000);
this.now = new Date().getTime();
const data = {
base: `${da.progressPercent.toFixed(2)}%`,
@@ -77,8 +83,10 @@ export class DifficultyMiningComponent implements OnInit {
newDifficultyHeight: da.nextRetargetHeight,
estimatedRetargetDate: da.estimatedRetargetDate,
previousRetarget: da.previousRetarget,
blocksUntilHalving,
timeUntilHalving,
blocksUntilHalving: this.blocksUntilHalving,
timeUntilHalving: this.timeUntilHalving,
timeAvg: da.timeAvg,
adjustedTimeAvg: da.adjustedTimeAvg,
};
return data;
})

View File

@@ -42,7 +42,7 @@
<div class="symbol" i18n="difficulty-box.average-block-time">Average block time</div>
</div>
<div class="item">
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text bigger" [ngStyle]="{'color': epochData.colorAdjustments}">
<div class="card-text bigger" [ngStyle]="{'color': epochData.colorAdjustments}">
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span>
@@ -52,9 +52,6 @@
{{ epochData.change | absolute | number: '1.2-2' }}
<span class="symbol">%</span>
</div>
<ng-template #recentlyAdjusted>
<div class="card-text">&#8212;</div>
</ng-template>
<div class="symbol">
<span i18n="difficulty-box.previous">Previous</span>:
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">

View File

@@ -19,6 +19,7 @@ interface EpochProgress {
blocksUntilHalving: number;
timeUntilHalving: number;
timeAvg: number;
adjustedTimeAvg: number;
}
type BlockStatus = 'mined' | 'behind' | 'ahead' | 'next' | 'remaining';
@@ -153,6 +154,7 @@ export class DifficultyComponent implements OnInit {
blocksUntilHalving,
timeUntilHalving,
timeAvg: da.timeAvg,
adjustedTimeAvg: da.adjustedTimeAvg,
};
return data;
})

View File

@@ -54,7 +54,7 @@
</form>
</div>
<div [class]="!widget ? 'chart' : 'chart-widget'" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div [class]="!widget ? 'chart' : 'chart-widget'" [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">

View File

@@ -57,8 +57,6 @@
}
.chart-widget {
width: 100%;
height: 100%;
height: 240px;
}
.pool-distribution {

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
import { echarts, EChartsOption } from '../../graphs/echarts';
import { merge, Observable, of } from 'rxjs';
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';
@@ -31,6 +31,7 @@ import { seoDescriptionNetwork } from '../../shared/common.utils';
export class HashrateChartComponent implements OnInit {
@Input() tableOnly = false;
@Input() widget = false;
@Input() height: number = 300;
@Input() right: number | string = 45;
@Input() left: number | string = 75;
@@ -86,28 +87,32 @@ export class HashrateChartComponent implements OnInit {
}
});
this.hashrateObservable$ = merge(
this.radioGroupForm.get('dateSpan').valueChanges
.pipe(
startWith(this.radioGroupForm.controls.dateSpan.value),
switchMap((timespan) => {
if (!this.widget && !firstRun) {
this.storageService.setValue('miningWindowPreference', timespan);
}
this.timespan = timespan;
firstRun = false;
this.miningWindowPreference = timespan;
this.isLoading = true;
return this.apiService.getHistoricalHashrate$(this.timespan);
})
),
this.stateService.chainTip$
this.hashrateObservable$ = combineLatest(
merge(
this.radioGroupForm.get('dateSpan').valueChanges
.pipe(
switchMap(() => {
startWith(this.radioGroupForm.controls.dateSpan.value),
switchMap((timespan) => {
if (!this.widget && !firstRun) {
this.storageService.setValue('miningWindowPreference', timespan);
}
this.timespan = timespan;
firstRun = false;
this.miningWindowPreference = timespan;
this.isLoading = true;
return this.apiService.getHistoricalHashrate$(this.timespan);
})
)
),
this.stateService.chainTip$
.pipe(
switchMap(() => {
return this.apiService.getHistoricalHashrate$(this.timespan);
})
)
),
fromEvent(window, 'resize').pipe(startWith(null)),
).pipe(
map(([response, _]) => response),
tap((response: any) => {
const data = response.body;
@@ -221,6 +226,7 @@ export class HashrateChartComponent implements OnInit {
]),
],
grid: {
height: (this.widget && this.height) ? this.height - 30 : undefined,
top: this.widget ? 20 : 40,
bottom: this.widget ? 30 : 70,
right: this.right,

View File

@@ -11,6 +11,13 @@ import { MiningService } from '../../services/mining.service';
import { download } from '../../shared/graphs.utils';
import { ActivatedRoute } from '@angular/router';
interface Hashrate {
timestamp: number;
avgHashRate: number;
share: number;
poolName: string;
}
@Component({
selector: 'app-hashrate-chart-pools',
templateUrl: './hashrate-chart-pools.component.html',
@@ -32,6 +39,7 @@ export class HashrateChartPoolsComponent implements OnInit {
miningWindowPreference: string;
radioGroupForm: UntypedFormGroup;
hashrates: Hashrate[];
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg',
@@ -87,56 +95,9 @@ export class HashrateChartPoolsComponent implements OnInit {
return this.apiService.getHistoricalPoolsHashrate$(timespan)
.pipe(
tap((response) => {
const hashrates = response.body;
this.hashrates = response.body;
// Prepare series (group all hashrates data point by pool)
const grouped = {};
for (const hashrate of hashrates) {
if (!grouped.hasOwnProperty(hashrate.poolName)) {
grouped[hashrate.poolName] = [];
}
grouped[hashrate.poolName].push(hashrate);
}
const series = [];
const legends = [];
for (const name in grouped) {
series.push({
zlevel: 0,
stack: 'Total',
name: name,
showSymbol: false,
symbol: 'none',
data: grouped[name].map((val) => [val.timestamp * 1000, val.share * 100]),
type: 'line',
lineStyle: { width: 0 },
areaStyle: { opacity: 1 },
smooth: true,
color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()],
emphasis: {
disabled: true,
scale: false,
},
});
legends.push({
name: name,
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
},
icon: 'roundRect',
itemStyle: {
color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()],
},
});
}
this.prepareChartOptions({
legends: legends,
series: series,
});
this.isLoading = false;
const series = this.applyHashrates();
if (series.length === 0) {
this.cd.markForCheck();
throw new Error();
@@ -156,6 +117,77 @@ export class HashrateChartPoolsComponent implements OnInit {
);
}
applyHashrates(): any[] {
const times: { [time: number]: { hashrates: { [pool: string]: Hashrate } } } = {};
const pools = {};
for (const hashrate of this.hashrates) {
if (!times[hashrate.timestamp]) {
times[hashrate.timestamp] = { hashrates: {} };
}
times[hashrate.timestamp].hashrates[hashrate.poolName] = hashrate;
if (!pools[hashrate.poolName]) {
pools[hashrate.poolName] = true;
}
}
const sortedTimes = Object.keys(times).sort((a,b) => parseInt(a) - parseInt(b)).map(time => ({ time: parseInt(time), hashrates: times[time].hashrates }));
const lastHashrates = sortedTimes[sortedTimes.length - 1].hashrates;
const sortedPools = Object.keys(pools).sort((a,b) => {
if (lastHashrates[b]?.share ?? lastHashrates[a]?.share ?? false) {
// sort by descending share of hashrate in latest period
return (lastHashrates[b]?.share || 0) - (lastHashrates[a]?.share || 0);
} else {
// tiebreak by pool name
b < a;
}
});
const series = [];
const legends = [];
for (const name of sortedPools) {
const data = sortedTimes.map(({ time, hashrates }) => {
return [time * 1000, (hashrates[name]?.share || 0) * 100];
});
series.push({
zlevel: 0,
stack: 'Total',
name: name,
showSymbol: false,
symbol: 'none',
data,
type: 'line',
lineStyle: { width: 0 },
areaStyle: { opacity: 1 },
smooth: true,
color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()],
emphasis: {
disabled: true,
scale: false,
},
});
legends.push({
name: name,
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
},
icon: 'roundRect',
itemStyle: {
color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()],
},
});
}
this.prepareChartOptions({
legends: legends,
series: series,
});
this.isLoading = false;
return series;
}
prepareChartOptions(data) {
let title: object;
if (data.series.length === 0) {
@@ -256,6 +288,7 @@ export class HashrateChartPoolsComponent implements OnInit {
},
}],
};
this.cd.markForCheck();
}
onChartInit(ec) {

View File

@@ -18,16 +18,15 @@ import { EChartsOption } from '../../graphs/echarts';
})
export class LbtcPegsGraphComponent implements OnInit, OnChanges {
@Input() data: any;
@Input() height: number | string = '320';
pegsChartOptions: EChartsOption;
height: number | string = '200';
right: number | string = '10';
top: number | string = '20';
left: number | string = '50';
template: ('widget' | 'advanced') = 'widget';
isLoading = true;
pegsChartOption: EChartsOption = {};
pegsChartInitOption = {
renderer: 'svg'
};
@@ -41,20 +40,24 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
}
ngOnChanges() {
if (!this.data) {
if (!this.data?.liquidPegs) {
return;
}
this.pegsChartOptions = this.createChartOptions(this.data.series, this.data.labels);
if (!this.data.liquidReserves) {
this.pegsChartOptions = this.createChartOptions(this.data.liquidPegs.series, this.data.liquidPegs.labels);
} else {
this.pegsChartOptions = this.createChartOptions(this.data.liquidPegs.series, this.data.liquidPegs.labels, this.data.liquidReserves.series);
}
}
rendered() {
if (!this.data) {
if (!this.data.liquidPegs) {
return;
}
this.isLoading = false;
}
createChartOptions(series: number[], labels: string[]): EChartsOption {
createChartOptions(pegSeries: number[], labels: string[], reservesSeries?: number[],): EChartsOption {
return {
grid: {
height: this.height,
@@ -99,17 +102,18 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
type: 'line',
},
formatter: (params: any) => {
const colorSpan = (color: string) => `<span class="indicator" style="background-color: #116761;"></span>`;
const colorSpan = (color: string) => `<span class="indicator" style="background-color: ${color};"></span>`;
let itemFormatted = '<div class="title">' + params[0].axisValue + '</div>';
params.map((item: any, index: number) => {
for (let index = params.length - 1; index >= 0; index--) {
const item = params[index];
if (index < 26) {
itemFormatted += `<div class="item">
<div class="indicator-container">${colorSpan(item.color)}</div>
<div class="grow"></div>
<div class="value">${formatNumber(item.value, this.locale, '1.2-2')} <span class="symbol">L-BTC</span></div>
<div style="margin-right: 5px"></div>
<div class="value">${formatNumber(item.value, this.locale, '1.2-2')} <span class="symbol">${item.seriesName}</span></div>
</div>`;
}
});
}
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}">${itemFormatted}</div>`;
}
},
@@ -138,20 +142,34 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
},
series: [
{
data: series,
data: pegSeries,
name: 'L-BTC',
color: '#116761',
type: 'line',
stack: 'total',
smooth: false,
smooth: true,
showSymbol: false,
areaStyle: {
opacity: 0.2,
color: '#116761',
},
lineStyle: {
width: 3,
width: 2,
color: '#116761',
},
},
{
data: reservesSeries,
name: 'BTC',
color: '#EA983B',
type: 'line',
smooth: true,
showSymbol: false,
lineStyle: {
width: 2,
color: '#EA983B',
},
},
],
};
}

View File

@@ -53,7 +53,7 @@
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['signet'] || '/signet')" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage + (networkPaths['bisq'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bisq" width="22" height="22" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
<a ngbDropdownItem *ngIf="env.BISQ_ENABLED" [href]="env.BISQ_WEBSITE_URL + urlLanguage + (networkPaths['bisq'] || '')" class="mainnet"><app-svg-images name="bisq" width="22" height="22" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
<a ngbDropdownItem class="liquid mr-1" [class.active]="network.val === 'liquid'" [routerLink]="networkPaths['liquid'] || '/'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
<a ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" [routerLink]="networkPaths['liquidtestnet'] || '/testnet'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
</div>
@@ -78,7 +78,7 @@
<li class="nav-item" routerLinkActive="active" id="btn-assets">
<a class="nav-link" [routerLink]="['/assets' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'database']" [fixedWidth]="true" i18n-title="master-page.assets" title="Assets"></fa-icon></a>
</li>
<li [hidden]="isMobile" class="nav-item mr-2" routerLinkActive="active" id="btn-docs">
<li class="nav-item mr-2" routerLinkActive="active" id="btn-docs">
<a class="nav-link" [routerLink]="['/docs' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'book']" [fixedWidth]="true" i18n-title="master-page.docs" title="Docs"></fa-icon></a>
</li>
<li class="nav-item" routerLinkActive="active" id="btn-about">
@@ -98,4 +98,4 @@
<app-global-footer *ngIf="footerVisible"></app-global-footer>
</ng-container>
</ng-container>

View File

@@ -23,6 +23,11 @@ li.nav-item {
margin: auto 10px;
padding-left: 10px;
padding-right: 10px;
@media (max-width: 429px) {
margin: auto 5px;
padding-left: 6px;
padding-right: 6px;
}
}
@media (min-width: 992px) {

View File

@@ -0,0 +1,72 @@
<div [ngClass]="{'widget': widget}">
<div class="clearfix"></div>
<div style="min-height: 295px">
<table class="table table-borderless">
<thead style="vertical-align: middle;">
<th class="address text-left" [ngClass]="{'widget': widget}" i18n="shared.address">Address</th>
<th class="amount text-right" [ngClass]="{'widget': widget}" i18n="address.balance">Balance</th>
</thead>
<tbody *ngIf="federationAddresses$ | async as addresses; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<ng-container *ngIf="widget; else regularRows">
<tr *ngFor="let address of addresses | slice:0:5">
<td class="address text-left widget">
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + address.bitcoinaddress }}" target="_blank" style="color:#b86d12">
<app-truncate [text]="address.bitcoinaddress" [lastChars]="6"></app-truncate>
</a>
</td>
<td class="amount text-right widget">
<app-amount [satoshis]="+address.balance" [noFiat]="true" [forceBtc]="true"></app-amount>
</td>
</tr>
</ng-container>
<ng-template #regularRows>
<tr *ngFor="let address of addresses | slice:(page - 1) * pageSize:page * pageSize">
<td class="address text-left">
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + address.bitcoinaddress }}" target="_blank" style="color:#b86d12">
<app-truncate [text]="address.bitcoinaddress" [lastChars]="6"></app-truncate>
</a>
</td>
<td class="amount text-right">
<app-amount [satoshis]="+address.balance" [noFiat]="true" [forceBtc]="true"></app-amount>
</td>
</tr>
</ng-template>
</tbody>
<ng-template #skeleton>
<tbody *ngIf="widget; else regularRowsSkeleton">
<tr *ngFor="let item of skeletonLines">
<td class="address text-left widget">
<span class="skeleton-loader" style="max-width: 400px"></span>
</td>
<td class="amount text-right widget">
<span class="skeleton-loader" style="max-width: 350px"></span>
</td>
</tr>
</tbody>
<ng-template #regularRowsSkeleton>
<tr *ngFor="let item of skeletonLines">
<td class="address text-left">
<span class="skeleton-loader" style="max-width: 600px"></span>
</td>
<td class="amount text-right">
<span class="skeleton-loader" style="max-width: 400px"></span>
</td>
</tr>
</ng-template>
</ng-template>
</table>
<ngb-pagination *ngIf="!widget && federationAddresses$ | async as addresses" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
[collectionSize]="addresses.length" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page"
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
</ngb-pagination>
<ng-template [ngIf]="!widget">
<div class="clearfix"></div>
<br>
</ng-template>
</div>
</div>

View File

@@ -0,0 +1,48 @@
.spinner-border {
height: 25px;
width: 25px;
margin-top: 13px;
}
tr, td, th {
border: 0px;
padding-top: 0.65rem;
padding-bottom: 0.6rem;
padding-right: 2rem;
.widget &.widget {
padding-right: 1rem;
@media (max-width: 510px) {
padding-right: 0.5rem;
}
}
}
.clear-link {
color: white;
}
.disabled {
pointer-events: none;
opacity: 0.5;
}
.progress {
background-color: #2d3348;
}
.address {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 160px;
}
.address.widget {
width: 60%;
}
.amount {
width: 25%;
}
.amount.widget {
width: 40%;
}

View File

@@ -0,0 +1,109 @@
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';
@Component({
selector: 'app-federation-addresses-list',
templateUrl: './federation-addresses-list.component.html',
styleUrls: ['./federation-addresses-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FederationAddressesListComponent implements OnInit {
@Input() widget: boolean = false;
@Input() federationAddresses$: Observable<FederationAddress[]>;
env: Env;
isLoading = true;
page = 1;
pageSize = 15;
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
skeletonLines: number[] = [];
auditStatus$: Observable<AuditStatus>;
auditUpdated$: Observable<boolean>;
lastReservesBlockUpdate: number = 0;
currentPeg$: Observable<CurrentPegs>;
lastPegBlockUpdate: number = 0;
lastPegAmount: string = '';
isLoad: boolean = true;
private destroy$ = new Subject();
constructor(
private apiService: ApiService,
public stateService: StateService,
private websocketService: WebsocketService
) {
}
ngOnInit(): void {
this.isLoading = !this.widget;
this.env = this.stateService.env;
this.skeletonLines = this.widget === true ? [...Array(5).keys()] : [...Array(15).keys()];
if (!this.widget) {
this.websocketService.want(['blocks']);
this.auditStatus$ = this.stateService.blocks$.pipe(
takeUntil(this.destroy$),
throttleTime(40000),
delayWhen(_ => this.isLoad ? timer(0) : timer(2000)),
tap(() => this.isLoad = false),
switchMap(() => this.apiService.federationAuditSynced$()),
shareReplay(1)
);
this.currentPeg$ = this.auditStatus$.pipe(
filter(auditStatus => auditStatus.isAuditSynced === true),
switchMap(_ =>
this.apiService.liquidPegs$().pipe(
filter((currentPegs) => currentPegs.lastBlockUpdate >= this.lastPegBlockUpdate),
tap((currentPegs) => {
this.lastPegBlockUpdate = currentPegs.lastBlockUpdate;
})
)
),
share()
);
this.auditUpdated$ = combineLatest([
this.auditStatus$,
this.currentPeg$
]).pipe(
filter(([auditStatus, _]) => auditStatus.isAuditSynced === true),
map(([auditStatus, currentPeg]) => ({
lastBlockAudit: auditStatus.lastBlockAudit,
currentPegAmount: currentPeg.amount
})),
switchMap(({ lastBlockAudit, currentPegAmount }) => {
const blockAuditCheck = lastBlockAudit > this.lastReservesBlockUpdate;
const amountCheck = currentPegAmount !== this.lastPegAmount;
this.lastReservesBlockUpdate = lastBlockAudit;
this.lastPegAmount = currentPegAmount;
return of(blockAuditCheck || amountCheck);
}),
share()
);
this.federationAddresses$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
throttleTime(40000),
switchMap(_ => this.apiService.federationAddresses$()),
tap(_ => this.isLoading = false),
share()
);
}
}
ngOnDestroy(): void {
this.destroy$.next(1);
this.destroy$.complete();
}
pageChange(page: number): void {
this.page = page;
}
}

View File

@@ -0,0 +1,31 @@
<div *ngIf="(federationWalletStats$ | async) as federationWalletStats; else loadingData">
<div class="fee-estimation-container">
<div class="item">
<a class="title-link" [routerLink]="['/audit/wallet/addresses' | relativeUrl]">
<h5 class="card-title"><ng-container i18n="liquid.federation-wallet">Liquid Federation Wallet</ng-container>&nbsp;<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
</a>
<div class="card-text">
<div class="fee-text">{{ federationWalletStats.address_count }} <span i18n="shared.addresses">addresses</span></div>
<div class="fiat">{{ federationWalletStats.utxo_count }} <span i18n="shared.utxos">UTXOs</span></div>
</div>
</div>
</div>
</div>
<ng-template #loadingData>
<div class="fee-estimation-container loading-container">
<div class="item">
<a class="title-link" [routerLink]="['/audit/wallet/addresses' | relativeUrl]">
<h5 class="card-title"><ng-container i18n="liquid.federation-wallet">Liquid Federation Wallet</ng-container>&nbsp;<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
</a>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
</div>
</ng-template>
<ng-template #loadingSkeleton>
<div class="skeleton-loader skeleton-loader-transactions" style="margin-top: 8px; margin-bottom: 8px;"></div>
</ng-template>

View File

@@ -0,0 +1,73 @@
.fee-estimation-container {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
@media (min-width: 376px) {
flex-direction: row;
}
.item {
max-width: 300px;
margin: 0;
width: -webkit-fill-available;
@media (min-width: 376px) {
margin: 0 auto 0px;
}
.card-title {
margin: 0;
color: #4a68b9;
font-size: 10px;
font-size: 1rem;
white-space: nowrap;
}
.card-text {
padding-top: 9px;
font-size: 22px;
span {
font-size: 11px;
position: relative;
top: -2px;
}
}
.card-text span {
color: #ffffff66;
font-size: 12px;
top: 0px;
}
.fee-text{
border-bottom: 1px solid #ffffff1c;
width: fit-content;
margin: auto;
line-height: 1.45;
padding: 0px 2px;
}
.fiat {
display: block;
font-size: 14px !important;
}
}
}
.card-text {
.skeleton-loader {
width: 100%;
display: block;
&:first-child {
max-width: 90px;
margin: 15px auto 3px;
}
&:last-child {
margin: 10px auto 3px;
max-width: 55px;
}
}
}
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
display: block;
margin-bottom: 4px;
text-decoration: none;
color: inherit;
}

View File

@@ -0,0 +1,31 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Observable, combineLatest, map, of } from 'rxjs';
@Component({
selector: 'app-federation-addresses-stats',
templateUrl: './federation-addresses-stats.component.html',
styleUrls: ['./federation-addresses-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FederationAddressesStatsComponent implements OnInit {
@Input() federationAddressesNumber$: Observable<number>;
@Input() federationUtxosNumber$: Observable<number>;
federationWalletStats$: Observable<any>;
constructor() { }
ngOnInit(): void {
this.federationWalletStats$ = combineLatest([
this.federationAddressesNumber$ ?? of(undefined),
this.federationUtxosNumber$ ?? of(undefined)
]).pipe(
map(([address_count, utxo_count]) => {
if (address_count === undefined || utxo_count === undefined) {
return undefined;
}
return { address_count, utxo_count}
})
)
}
}

View File

@@ -0,0 +1,109 @@
<div [ngClass]="{'widget': widget}">
<div class="clearfix"></div>
<div style="min-height: 295px">
<table class="table table-borderless">
<thead style="vertical-align: middle;">
<th class="txid text-left" [ngClass]="{'widget': widget}" i18n="transaction.output">Output</th>
<th class="address text-left" *ngIf="!widget" i18n="shared.address">Address</th>
<th class="amount text-right" [ngClass]="{'widget': widget}" i18n="shared.amount">Amount</th>
<th class="pegin text-left" *ngIf="!widget" i18n="liquid.related-peg-in">Related Peg-In</th>
<th class="timestamp text-left" i18n="shared.date" [ngClass]="{'widget': widget}">Date</th>
</thead>
<tbody *ngIf="federationUtxos$ | async as utxos; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<ng-container *ngIf="widget; else regularRows">
<tr *ngFor="let utxo of utxos | slice:0:6">
<td class="txid text-left widget">
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/tx/' + utxo.txid + ':' + utxo.txindex }}" target="_blank" style="color:#b86d12">
<app-truncate [text]="utxo.txid + ':' + utxo.txindex" [lastChars]="6"></app-truncate>
</a>
</td>
<td class="amount text-right widget">
<app-amount [satoshis]="utxo.amount" [noFiat]="true" [forceBtc]="true"></app-amount>
</td>
<td class="timestamp text-left widget">
<app-time kind="since" [time]="utxo.blocktime"></app-time>
</td>
</tr>
</ng-container>
<ng-template #regularRows>
<tr *ngFor="let utxo of utxos | slice:(page - 1) * pageSize:page * pageSize">
<td class="txid text-left">
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/tx/' + utxo.txid + ':' + utxo.txindex }}" target="_blank" style="color:#b86d12">
<app-truncate [text]="utxo.txid + ':' + utxo.txindex" [lastChars]="6"></app-truncate>
</a>
</td>
<td class="address text-left">
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + utxo.bitcoinaddress }}" target="_blank" style="color:#b86d12">
<app-truncate [text]="utxo.bitcoinaddress" [lastChars]="6"></app-truncate>
</a>
</td>
<td class="amount text-right">
<app-amount [satoshis]="utxo.amount" [noFiat]="true" [forceBtc]="true"></app-amount>
</td>
<td class="pegin text-left">
<ng-container *ngIf="utxo.pegtxid; else noPeginMessage">
<a [routerLink]="['/tx' | relativeUrl, utxo.pegtxid]" [fragment]="'vin=' + utxo.pegindex">
<app-truncate [text]="utxo.pegtxid + ':' + utxo.pegindex" [lastChars]="6"></app-truncate>
</a>
</ng-container>
<ng-template #noPeginMessage>
<i><span class="text-muted" i18n="liquid.change-output">Change output</span></i>
</ng-template>
</td>
<td class="timestamp text-left">
&lrm;{{ utxo.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="symbol lg-inline relative-time"><i>(<app-time kind="since" [time]="utxo.blocktime"></app-time>)</i></div>
</td>
</tr>
</ng-template>
</tbody>
<ng-template #skeleton>
<tbody *ngIf="widget; else regularRowsSkeleton">
<tr *ngFor="let item of skeletonLines">
<td class="txid text-left widget">
<span class="skeleton-loader" style="max-width: 400px"></span>
</td>
<td class="amount text-right widget">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
<td class="timestamp text-left widget">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
</tr>
</tbody>
<ng-template #regularRowsSkeleton>
<tr *ngFor="let item of skeletonLines">
<td class="txid text-left">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
<td class="address text-left">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
<td class="amount text-right">
<span class="skeleton-loader" style="max-width: 140px"></span>
</td>
<td class="pegin text-left">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
<td class="timestamp text-left">
<span class="skeleton-loader" style="max-width: 140px"></span>
</td>
</tr>
</ng-template>
</ng-template>
</table>
<ngb-pagination *ngIf="!widget && federationUtxos$ | async as utxos" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
[collectionSize]="utxos.length" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page"
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
</ngb-pagination>
<ng-template [ngIf]="!widget">
<div class="clearfix"></div>
<br>
</ng-template>
</div>
</div>

View File

@@ -0,0 +1,94 @@
.spinner-border {
height: 25px;
width: 25px;
margin-top: 13px;
}
tr, td, th {
border: 0px;
padding-top: 0.65rem !important;
padding-bottom: 0.6rem !important;
padding-right: 2rem !important;
.widget {
padding-right: 1rem !important;
}
}
.clear-link {
color: white;
}
.disabled {
pointer-events: none;
opacity: 0.5;
}
.progress {
background-color: #2d3348;
}
.txid {
width: 25%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 160px;
}
.txid.widget {
width: 40%;
}
.address {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 160px;
@media (max-width: 527px) {
display: none;
}
}
.amount {
width: 12%;
}
.amount.widget {
width: 30%;
}
.pegin {
width: 25%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 160px;
@media (max-width: 872px) {
display: none;
}
}
.timestamp {
width: 18%;
@media (max-width: 800px) {
display: none;
}
@media (max-width: 1000px) {
.relative-time {
display: none;
}
}
}
.timestamp.widget {
width: 100%;
@media (min-width: 768px) AND (max-width: 1050px) {
display: none;
}
@media (max-width: 767px) {
display: block;
}
@media (max-width: 500px) {
display: none;
}
}

View File

@@ -0,0 +1,109 @@
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, FederationUtxo } from '../../../interfaces/node-api.interface';
import { WebsocketService } from '../../../services/websocket.service';
@Component({
selector: 'app-federation-utxos-list',
templateUrl: './federation-utxos-list.component.html',
styleUrls: ['./federation-utxos-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FederationUtxosListComponent implements OnInit {
@Input() widget: boolean = false;
@Input() federationUtxos$: Observable<FederationUtxo[]>;
env: Env;
isLoading = true;
page = 1;
pageSize = 15;
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
skeletonLines: number[] = [];
auditStatus$: Observable<AuditStatus>;
auditUpdated$: Observable<boolean>;
lastReservesBlockUpdate: number = 0;
currentPeg$: Observable<CurrentPegs>;
lastPegBlockUpdate: number = 0;
lastPegAmount: string = '';
isLoad: boolean = true;
private destroy$ = new Subject();
constructor(
private apiService: ApiService,
public stateService: StateService,
private websocketService: WebsocketService,
) {
}
ngOnInit(): void {
this.isLoading = !this.widget;
this.env = this.stateService.env;
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
if (!this.widget) {
this.websocketService.want(['blocks']);
this.auditStatus$ = this.stateService.blocks$.pipe(
takeUntil(this.destroy$),
throttleTime(40000),
delayWhen(_ => this.isLoad ? timer(0) : timer(2000)),
tap(() => this.isLoad = false),
switchMap(() => this.apiService.federationAuditSynced$()),
shareReplay(1)
);
this.currentPeg$ = this.auditStatus$.pipe(
filter(auditStatus => auditStatus.isAuditSynced === true),
switchMap(_ =>
this.apiService.liquidPegs$().pipe(
filter((currentPegs) => currentPegs.lastBlockUpdate >= this.lastPegBlockUpdate),
tap((currentPegs) => {
this.lastPegBlockUpdate = currentPegs.lastBlockUpdate;
})
)
),
share()
);
this.auditUpdated$ = combineLatest([
this.auditStatus$,
this.currentPeg$
]).pipe(
filter(([auditStatus, _]) => auditStatus.isAuditSynced === true),
map(([auditStatus, currentPeg]) => ({
lastBlockAudit: auditStatus.lastBlockAudit,
currentPegAmount: currentPeg.amount
})),
switchMap(({ lastBlockAudit, currentPegAmount }) => {
const blockAuditCheck = lastBlockAudit > this.lastReservesBlockUpdate;
const amountCheck = currentPegAmount !== this.lastPegAmount;
this.lastReservesBlockUpdate = lastBlockAudit;
this.lastPegAmount = currentPegAmount;
return of(blockAuditCheck || amountCheck);
}),
share()
);
this.federationUtxos$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
throttleTime(40000),
switchMap(_ => this.apiService.federationUtxos$()),
tap(_ => this.isLoading = false),
share()
);
}
}
ngOnDestroy(): void {
this.destroy$.next(1);
this.destroy$.complete();
}
pageChange(page: number): void {
this.page = page;
}
}

View File

@@ -0,0 +1,24 @@
<div class="container-xl">
<div>
<h1 i18n="liquid.federation-wallet">Liquid Federation Wallet</h1>
</div>
<div class="nav-container">
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link" [routerLink]="['/audit/wallet/utxos' | relativeUrl]" routerLinkActive="active">UTXOs</a>
</li>
<li class="nav-item">
<a class="nav-link" [routerLink]="['/audit/wallet/addresses' | relativeUrl]" routerLinkActive="active"><ng-container i18n="mining.addresses">Addresses</ng-container></a>
</li>
</ul>
</div>
<div class="clearfix"></div>
<router-outlet></router-outlet>
</div>
<br>

View File

@@ -0,0 +1,13 @@
ul {
margin-bottom: 20px;
}
@media (max-width: 767.98px) {
.nav-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: auto;
}
}

View File

@@ -0,0 +1,20 @@
import { Component, OnInit } from '@angular/core';
import { SeoService } from '../../../services/seo.service';
@Component({
selector: 'app-federation-wallet',
templateUrl: './federation-wallet.component.html',
styleUrls: ['./federation-wallet.component.scss']
})
export class FederationWalletComponent implements OnInit {
constructor(
private seoService: SeoService
) {
this.seoService.setTitle($localize`:@@993e5bc509c26db81d93018e24a6afe6e50cae52:Liquid Federation Wallet`);
}
ngOnInit(): void {
}
}

View File

@@ -0,0 +1,137 @@
<div [ngClass]="{'container-xl': !widget, 'widget': widget}">
<div *ngIf="!widget">
<h1 i18n="liquid.recent-pegs">Recent Peg-In / Out's</h1>
</div>
<div class="clearfix"></div>
<div style="min-height: 295px">
<table class="table table-borderless">
<thead style="vertical-align: middle;">
<th class="transaction text-left" [ngClass]="{'widget': widget}" i18n="shared.transaction">Transaction</th>
<th class="timestamp text-left" i18n="shared.date" [ngClass]="{'widget': widget}">Date</th>
<th class="amount text-right" [ngClass]="{'widget': widget}" i18n="shared.amount">Amount</th>
<th class="output text-left" *ngIf="!widget" i18n="liquid.fund-redemption-tx">Fund / Redemption Tx</th>
<th class="address text-left" *ngIf="!widget" i18n="liquid.bitcoin-address">BTC Address</th>
</thead>
<tbody *ngIf="recentPegsList$ | async as pegs; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<ng-container *ngIf="widget; else regularRows">
<tr *ngFor="let peg of pegs | slice:0:5">
<td class="transaction text-left widget">
<ng-container *ngIf="peg.amount > 0">
<a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vin=' + peg.txindex">
<app-truncate [text]="peg.txid"></app-truncate>
</a>
</ng-container>
<ng-container *ngIf="peg.amount < 0">
<a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vout=' + peg.txindex">
<app-truncate [text]="peg.txid"></app-truncate>
</a>
</ng-container>
</td>
<td class="timestamp text-left widget">
<app-time kind="since" [time]="peg.blocktime"></app-time>
</td>
<td class="amount text-right widget" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0, 'glow-effect': peg.amount < 0 && peg.bitcoinaddress && !peg.bitcointxid}">
<app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount>
</td>
</tr>
</ng-container>
<ng-template #regularRows>
<tr *ngFor="let peg of pegs;">
<td class="transaction text-left">
<ng-container *ngIf="peg.amount > 0">
<a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vin=' + peg.txindex">
<app-truncate [text]="peg.txid" [lastChars]="6"></app-truncate>
</a>
</ng-container>
<ng-container *ngIf="peg.amount < 0">
<a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vout=' + peg.txindex">
<app-truncate [text]="peg.txid" [lastChars]="6"></app-truncate>
</a>
</ng-container>
</td>
<td class="timestamp text-left">
&lrm;{{ peg.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="symbol lg-inline relative-time"><i>(<app-time kind="since" [time]="peg.blocktime"></app-time>)</i></div>
</td>
<td class="amount text-right" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0, 'glow-effect': peg.amount < 0 && peg.bitcoinaddress && !peg.bitcointxid}">
<app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount>
</td>
<td class="output text-left">
<ng-container *ngIf="peg.bitcointxid; else redeemInProgress">
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/tx/' + peg.bitcointxid + ':' + peg.bitcoinindex }}" target="_blank" style="color:#b86d12">
<app-truncate [text]="peg.bitcointxid + ':' + peg.bitcoinindex" [lastChars]="6"></app-truncate>
</a>
</ng-container>
<ng-template #redeemInProgress>
<ng-container *ngIf="peg.bitcoinaddress; else noRedeem">
<i><span class="text-muted" i18n="liquid.redemption-in-progress">Peg out in progress...</span></i>
</ng-container>
</ng-template>
</td>
<td class="address text-left">
<ng-container *ngIf="peg.bitcoinaddress; else noRedeem">
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + peg.bitcoinaddress }}" target="_blank" style="color:#b86d12">
<app-truncate [text]="peg.bitcoinaddress" [lastChars]="6"></app-truncate>
</a>
</ng-container>
</td>
</tr>
</ng-template>
</tbody>
<ng-template #skeleton>
<tbody *ngIf="widget; else regularRowsSkeleton">
<tr *ngFor="let item of skeletonLines">
<td class="transaction text-left widget">
<span class="skeleton-loader" style="max-width: 400px"></span>
</td>
<td class="timestamp text-left widget">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
<td class="amount text-right widget">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
</tr>
</tbody>
<ng-template #regularRowsSkeleton>
<tr *ngFor="let item of skeletonLines">
<td class="transaction text-left">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
<td class="timestamp text-left">
<span class="skeleton-loader" style="max-width: 240px"></span>
</td>
<td class="amount text-right">
<span class="skeleton-loader" style="max-width: 140px"></span>
</td>
<td class="output text-left">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
<td class="address text-left">
<span class="skeleton-loader" style="max-width: 240px"></span>
</td>
</tr>
</ng-template>
</ng-template>
</table>
<ngb-pagination *ngIf="!widget && pegsCount$ | async as pegsCount" class="pagination-container float-right mt-2" [class]="isLoading || isPegCountLoading ? 'disabled' : ''"
[collectionSize]="pegsCount" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page"
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
</ngb-pagination>
<ng-template [ngIf]="!widget">
<div class="clearfix"></div>
<br>
</ng-template>
</div>
</div>
<br>
<ng-template #noRedeem>
<span class="text-muted">-</span>
</ng-template>

View File

@@ -0,0 +1,123 @@
.spinner-border {
height: 25px;
width: 25px;
margin-top: 13px;
}
tr, td, th {
border: 0px;
padding-top: 0.65rem;
padding-bottom: 0.6rem;
padding-right: 2rem;
.widget &.widget {
padding-right: 1rem;
@media (max-width: 510px) {
padding-right: 0.5rem;
}
}
}
.clear-link {
color: white;
}
.disabled {
pointer-events: none;
opacity: 0.5;
}
.progress {
background-color: #2d3348;
}
.transaction {
width: 20%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 120px;
}
.transaction.widget {
width: 100%;
}
.address {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 160px;
@media (max-width: 527px) {
display: none;
}
}
.amount {
width: 0%;
}
.output {
width: 20%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 160px;
@media (max-width: 800px) {
display: none;
}
}
.address {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 160px;
@media (max-width: 960px) {
display: none;
}
}
.timestamp {
width: 0%;
@media (max-width: 650px) {
display: none;
}
@media (max-width: 1000px) {
.relative-time {
display: none;
}
}
}
.timestamp.widget {
@media (min-width: 768px) AND (max-width: 1050px) {
display: none;
}
@media (max-width: 767px) {
display: block;
}
@media (max-width: 510px) {
display: none;
}
}
.credit {
color: #7CB342;
}
.debit {
color: #D81B60;
}
.glow-effect {
animation: color-oscillation 1s ease-in-out infinite alternate;
}
@keyframes color-oscillation {
0% {
color: #777983;
}
100% {
color: #D81B60;
}
}

View File

@@ -0,0 +1,139 @@
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
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, RecentPeg } from '../../../interfaces/node-api.interface';
import { WebsocketService } from '../../../services/websocket.service';
import { SeoService } from '../../../services/seo.service';
@Component({
selector: 'app-recent-pegs-list',
templateUrl: './recent-pegs-list.component.html',
styleUrls: ['./recent-pegs-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RecentPegsListComponent implements OnInit {
@Input() widget: boolean = false;
@Input() recentPegsList$: Observable<RecentPeg[]>;
env: Env;
isLoading = true;
isPegCountLoading = true;
page = 1;
pageSize = 15;
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
skeletonLines: number[] = [];
auditStatus$: Observable<AuditStatus>;
auditUpdated$: Observable<boolean>;
lastReservesBlockUpdate: number = 0;
currentPeg$: Observable<CurrentPegs>;
pegsCount$: Observable<number>;
startingIndexSubject: BehaviorSubject<number> = new BehaviorSubject(0);
currentIndex: number = 0;
lastPegBlockUpdate: number = 0;
lastPegAmount: string = '';
isLoad: boolean = true;
private destroy$ = new Subject();
constructor(
private apiService: ApiService,
public stateService: StateService,
private websocketService: WebsocketService,
private seoService: SeoService
) {
}
ngOnInit(): void {
this.isLoading = !this.widget;
this.env = this.stateService.env;
this.skeletonLines = this.widget === true ? [...Array(5).keys()] : [...Array(15).keys()];
if (!this.widget) {
this.seoService.setTitle($localize`:@@a8b0889ea1b41888f1e247f2731cc9322198ca04:Recent Peg-In / Out's`);
this.websocketService.want(['blocks']);
this.auditStatus$ = this.stateService.blocks$.pipe(
takeUntil(this.destroy$),
throttleTime(40000),
delayWhen(_ => this.isLoad ? timer(0) : timer(2000)),
tap(() => this.isLoad = false),
switchMap(() => this.apiService.federationAuditSynced$()),
shareReplay(1)
);
this.currentPeg$ = this.auditStatus$.pipe(
filter(auditStatus => auditStatus.isAuditSynced === true),
switchMap(_ =>
this.apiService.liquidPegs$().pipe(
filter((currentPegs) => currentPegs.lastBlockUpdate >= this.lastPegBlockUpdate),
tap((currentPegs) => {
this.lastPegBlockUpdate = currentPegs.lastBlockUpdate;
})
)
),
share()
);
this.auditUpdated$ = combineLatest([
this.auditStatus$,
this.currentPeg$
]).pipe(
filter(([auditStatus, _]) => auditStatus.isAuditSynced === true),
map(([auditStatus, currentPeg]) => ({
lastBlockAudit: auditStatus.lastBlockAudit,
currentPegAmount: currentPeg.amount
})),
switchMap(({ lastBlockAudit, currentPegAmount }) => {
const blockAuditCheck = lastBlockAudit > this.lastReservesBlockUpdate;
const amountCheck = currentPegAmount !== this.lastPegAmount;
this.lastReservesBlockUpdate = lastBlockAudit;
this.lastPegAmount = currentPegAmount;
return of(blockAuditCheck || amountCheck);
}),
share()
);
this.pegsCount$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
tap(() => this.isPegCountLoading = true),
switchMap(_ => this.apiService.pegsCount$()),
map((data) => data.pegs_count),
tap(() => this.isPegCountLoading = false),
share()
);
this.recentPegsList$ = combineLatest([
this.auditStatus$,
this.auditUpdated$,
this.startingIndexSubject
]).pipe(
filter(([auditStatus, auditUpdated, startingIndex]) => {
const auditStatusCheck = auditStatus.isAuditSynced === true;
const auditUpdatedCheck = auditUpdated === true;
const startingIndexCheck = startingIndex !== this.currentIndex;
return auditStatusCheck && (auditUpdatedCheck || startingIndexCheck);
}),
tap(([_, __, startingIndex]) => {
this.currentIndex = startingIndex;
this.isLoading = true;
}),
switchMap(([_, __, startingIndex]) => this.apiService.recentPegsList$(startingIndex)),
tap(() => this.isLoading = false),
share()
);
}
}
ngOnDestroy(): void {
this.destroy$.next(1);
this.destroy$.complete();
}
pageChange(page: number): void {
this.startingIndexSubject.next((page - 1) * 15);
this.page = page;
}
}

View File

@@ -0,0 +1,47 @@
<div *ngIf="(pegsVolume$ | async) as pegsVolume; else loadingData">
<div class="fee-estimation-container">
<div class="item">
<a class="title-link" [routerLink]="['/audit/pegs' | relativeUrl]">
<h5 class="card-title"><ng-container i18n="liquid.recent-pegs">Recent Peg-In / Out's</ng-container>&nbsp;<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
</a>
</div>
</div>
<div class="fee-estimation-container">
<div class="item">
<div class="card-text">
<div class="fee-text credit" i18n-ngbTooltip="liquid.peg-ins-volume-day" ngbTooltip="24h Peg-In Volume" placement="top">+{{ (+pegsVolume[0].volume) / 100000000 | number: '1.2-2' }} <span i18n="shared.addresses">BTC</span></div>
<div class="fiat">{{ (+pegsVolume[0].number) }} <span i18n="liquid.peg-ins">Peg-Ins</span></div>
</div>
</div>
<div class="item">
<div class="card-text">
<div class="fee-text debit" i18n-ngbTooltip="liquid.peg-out-volume-day" ngbTooltip="24h Peg-Out Volume" placement="top">{{ (+pegsVolume[1].volume) / 100000000 | number: '1.2-2' }} <span i18n="shared.addresses">BTC</span></div>
<div class="fiat">{{ (+pegsVolume[1].number) }} <span i18n="liquid.peg-outs">Peg-Outs</span></div>
</div>
</div>
</div>
</div>
<ng-template #loadingData>
<div class="fee-estimation-container loading-container">
<div class="item">
<a class="title-link" [routerLink]="['/audit/pegs' | relativeUrl]">
<h5 class="card-title"><ng-container i18n="liquid.recent-pegs">Recent Peg-In / Out's</ng-container>&nbsp;<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
</a>
</div>
</div>
<div class="fee-estimation-container">
<div class="item">
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
</div>
</ng-template>

View File

@@ -0,0 +1,80 @@
.fee-estimation-container {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
@media (min-width: 376px) {
flex-direction: row;
}
.item {
max-width: 300px;
margin: 0;
width: -webkit-fill-available;
@media (min-width: 376px) {
margin: 0 auto 0px;
}
.card-title {
margin: 0;
color: #4a68b9;
font-size: 10px;
font-size: 1rem;
white-space: nowrap;
}
.card-text {
font-size: 22px;
span {
font-size: 11px;
position: relative;
top: -2px;
}
}
.card-text span {
color: #ffffff66;
font-size: 12px;
top: 0px;
}
.fee-text{
border-bottom: 1px solid #ffffff1c;
width: fit-content;
margin: auto;
line-height: 1.45;
padding: 0px 2px;
}
.fiat {
display: block;
font-size: 14px !important;
}
}
}
.card-text {
.skeleton-loader {
width: 100%;
display: block;
&:first-child {
max-width: 90px;
margin: 15px auto 3px;
}
&:last-child {
margin: 10px auto 3px;
max-width: 55px;
}
}
}
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
display: block;
margin-bottom: 4px;
text-decoration: none;
color: inherit;
}
.credit {
color: #7CB342;
}
.debit {
color: #D81B60;
}

View File

@@ -0,0 +1,19 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { PegsVolume } from '../../../interfaces/node-api.interface';
@Component({
selector: 'app-recent-pegs-stats',
templateUrl: './recent-pegs-stats.component.html',
styleUrls: ['./recent-pegs-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RecentPegsStatsComponent implements OnInit {
@Input() pegsVolume$: Observable<PegsVolume[]>;
constructor() { }
ngOnInit(): void {
}
}

View File

@@ -0,0 +1,42 @@
<div *ngIf="(unbackedMonths$ | async) as unbackedMonths; else loadingData">
<ng-container *ngIf="unbackedMonths.historyComplete; else loadingData">
<div class="fee-estimation-container">
<div class="item">
<h5 class="card-title" i18n="liquid.unpeg">Unpeg</h5>
<div class="card-text">
<div class="fee-text" [ngClass]="{'danger' : unbackedMonths.total > 0, 'correct': unbackedMonths.total === 0}">
{{ unbackedMonths.total }} <span i18n="liquid.unpeg-event">Unpeg Event</span>
</div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="liquid.avg-peg-ratio">Avg Peg Ratio</h5>
<div class="card-text">
<div class="fee-text" [ngClass]="{'danger' : unbackedMonths.avg < 1, 'correct': unbackedMonths.avg >= 1}">
{{ (unbackedMonths.avg * 100).toFixed(3) }} %
</div>
</div>
</div>
</div>
</ng-container>
</div>
<ng-template #loadingData>
<div class="fee-estimation-container loading-container">
<div class="item">
<h5 class="card-title" i18n="liquid.unpeg">Unpeg</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="liquid.avg-peg-ratio">Avg Peg Ratio</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
</div>
</div>
</div>
</ng-template>

View File

@@ -0,0 +1,63 @@
.fee-estimation-container {
display: flex;
justify-content: space-between;
@media (min-width: 376px) {
flex-direction: row;
}
.item {
max-width: 300px;
margin: 0;
width: -webkit-fill-available;
@media (min-width: 376px) {
margin: 0 auto 0px;
}
.card-title {
margin-bottom: 4px;
color: #4a68b9;
font-size: 10px;
font-size: 1rem;
white-space: nowrap;
}
.card-text {
font-size: 22px;
span {
font-size: 11px;
position: relative;
top: -2px;
}
.danger {
color: #D81B60;
}
.correct {
color: #7CB342;
}
}
.card-text span {
color: #ffffff66;
font-size: 12px;
top: 0px;
}
.fee-text{
width: fit-content;
margin: auto;
line-height: 1.45;
padding: 0px 2px;
}
}
}
.loading-container{
min-height: 76px;
}
.card-text {
.skeleton-loader {
width: 100%;
display: block;
max-width: 90px;
margin: 15px auto 3px;
}
}

View File

@@ -0,0 +1,51 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Observable, map } from 'rxjs';
@Component({
selector: 'app-reserves-ratio-stats',
templateUrl: './reserves-ratio-stats.component.html',
styleUrls: ['./reserves-ratio-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReservesRatioStatsComponent implements OnInit {
@Input() fullHistory$: Observable<any>;
unbackedMonths$: Observable<any>
constructor() { }
ngOnInit(): void {
if (!this.fullHistory$) {
return;
}
this.unbackedMonths$ = this.fullHistory$
.pipe(
map((fullHistory) => {
if (fullHistory.liquidPegs.series.length !== fullHistory.liquidReserves.series.length) {
return {
historyComplete: false,
total: null
};
}
// Only check the last 3 years
let ratioSeries = fullHistory.liquidReserves.series.map((value: number, index: number) => value / fullHistory.liquidPegs.series[index]);
ratioSeries = ratioSeries.slice(Math.max(ratioSeries.length - 36, 0));
let total = 0;
let avg = 0;
for (let i = 0; i < ratioSeries.length; i++) {
avg += ratioSeries[i];
if (ratioSeries[i] < 1) {
total++;
}
}
avg = avg / ratioSeries.length;
return {
historyComplete: true,
total: total,
avg: avg,
};
})
);
}
}

View File

@@ -0,0 +1,4 @@
<div class="echarts" echarts [initOpts]="ratioChartInitOptions" [options]="ratioChartOptions" (chartRendered)="rendered()"></div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>
</div>

View File

@@ -0,0 +1,6 @@
.loadingGraphs {
position: absolute;
top: 50%;
left: calc(50% - 16px);
z-index: 100;
}

View File

@@ -0,0 +1,175 @@
import { Component, ChangeDetectionStrategy, Input, OnChanges, OnInit, HostListener } from '@angular/core';
import { EChartsOption } from '../../../graphs/echarts';
import { CurrentPegs } from '../../../interfaces/node-api.interface';
@Component({
selector: 'app-reserves-ratio',
templateUrl: './reserves-ratio.component.html',
styleUrls: ['./reserves-ratio.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReservesRatioComponent implements OnInit, OnChanges {
@Input() currentPeg: CurrentPegs;
@Input() currentReserves: CurrentPegs;
ratioChartOptions: EChartsOption;
height: number | string = '200';
right: number | string = '10';
top: number | string = '20';
left: number | string = '50';
template: ('widget' | 'advanced') = 'widget';
isLoading = true;
ratioChartInitOptions = {
renderer: 'svg'
};
constructor() { }
ngOnInit() {
this.isLoading = true;
}
ngOnChanges() {
this.updateChartOptions();
}
updateChartOptions() {
if (!this.currentPeg || !this.currentReserves || this.currentPeg.amount === '0') {
return;
}
this.ratioChartOptions = this.createChartOptions(this.currentPeg, this.currentReserves);
}
rendered() {
if (!this.currentPeg || !this.currentReserves) {
return;
}
this.isLoading = false;
}
createChartOptions(currentPeg: CurrentPegs, currentReserves: CurrentPegs): EChartsOption {
const value = parseFloat(currentReserves.amount) / parseFloat(currentPeg.amount);
const hideMaxAxisLabels = value >= 1.001;
const hideMinAxisLabels = value <= 0.999;
let axisFontSize = 14;
let pointerLength = '50%';
let pointerWidth = 16;
let offsetCenter = ['0%', '-22%'];
if (window.innerWidth >= 992) {
axisFontSize = 14;
pointerLength = '50%';
pointerWidth = 16;
offsetCenter = value >= 1.0007 || value <= 0.9993 ? ['0%', '-30%'] : ['0%', '-22%'];
} else if (window.innerWidth >= 768) {
axisFontSize = 10;
pointerLength = '35%';
pointerWidth = 12;
offsetCenter = value >= 1.0007 || value <= 0.9993 ? ['0%', '-37%'] : ['0%', '-27%'];
} else if (window.innerWidth >= 450) {
axisFontSize = 14;
pointerLength = '45%';
pointerWidth = 14;
offsetCenter = value >= 1.0007 || value <= 0.9993 ? ['0%', '-32%'] : ['0%', '-22%'];
} else {
axisFontSize = 10;
pointerLength = '35%';
pointerWidth = 12;
offsetCenter = value >= 1.0007 || value <= 0.9993 ? ['0%', '-37%'] : ['0%', '-27%'];
}
return {
series: [
{
type: 'gauge',
startAngle: 180,
endAngle: 0,
center: ['50%', '75%'],
radius: '100%',
min: 0.999,
max: 1.001,
splitNumber: 2,
axisLine: {
lineStyle: {
width: 6,
color: [
[0.49, '#D81B60'],
[1, '#7CB342']
]
}
},
axisLabel: {
color: 'inherit',
fontFamily: 'inherit',
fontSize: axisFontSize,
formatter: function (value) {
if (value === 0.999) {
return hideMinAxisLabels ? '' : '99.9%';
} else if (value === 1.001) {
return hideMaxAxisLabels ? '' : '100.1%';
} else {
return '100%';
}
},
},
pointer: {
icon: 'path://M2090.36389,615.30999 L2090.36389,615.30999 C2091.48372,615.30999 2092.40383,616.194028 2092.44859,617.312956 L2096.90698,728.755929 C2097.05155,732.369577 2094.2393,735.416212 2090.62566,735.56078 C2090.53845,735.564269 2090.45117,735.566014 2090.36389,735.566014 L2090.36389,735.566014 C2086.74736,735.566014 2083.81557,732.63423 2083.81557,729.017692 C2083.81557,728.930412 2083.81732,728.84314 2083.82081,728.755929 L2088.2792,617.312956 C2088.32396,616.194028 2089.24407,615.30999 2090.36389,615.30999 Z',
length: pointerLength,
width: pointerWidth,
offsetCenter: offsetCenter,
itemStyle: {
color: 'auto'
}
},
axisTick: {
length: 12,
lineStyle: {
color: 'auto',
width: 2
}
},
splitLine: {
length: 20,
lineStyle: {
color: 'auto',
width: 5
}
},
title: {
show: true,
offsetCenter: [0, '-127%'],
fontSize: 18,
color: '#4a68b9',
fontFamily: 'inherit',
fontWeight: 500,
},
detail: {
fontSize: 25,
offsetCenter: [0, '-0%'],
valueAnimation: true,
fontFamily: 'inherit',
fontWeight: 500,
formatter: function (value) {
return (value * 100).toFixed(3) + '%';
},
color: 'inherit'
},
data: [
{
value: value,
name: 'Assets vs Liabilities'
}
]
}
]
};
}
@HostListener('window:resize', ['$event'])
onResize(): void {
this.updateChartOptions();
}
}

View File

@@ -0,0 +1,29 @@
<div class="fee-estimation-container">
<div class="item">
<h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5>
<div *ngIf="(currentPeg$ | async) as currentPeg; else loadingData" class="card-text">
<div class="fee-text">{{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} <span>L-BTC</span></div>
<span class="fiat">
<span><ng-container i18n="shared.as-of-block">As of block</ng-container>&nbsp;<a [routerLink]="['/block', currentPeg.hash]">{{ currentPeg.lastBlockUpdate }}</a></span>
</span>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5>
<div *ngIf="(currentReserves$ | async) as currentReserves; else loadingData" class="card-text">
<div class="fee-text">{{ (+currentReserves.amount) / 100000000 | number: '1.2-2' }} <span style="color: #b86d12;">BTC</span></div>
<span class="fiat">
<span><ng-container i18n="shared.as-of-block">As of block</ng-container>&nbsp;<a href="{{ env.MEMPOOL_WEBSITE_URL + '/block/' + currentReserves.hash }}" target="_blank" style="color:#b86d12">{{ currentReserves.lastBlockUpdate }}</a></span>
</span>
</div>
</div>
</div>
<ng-template #loadingData>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</ng-template>

View File

@@ -0,0 +1,74 @@
.fee-estimation-container {
display: flex;
justify-content: space-between;
@media (min-width: 376px) {
flex-direction: row;
}
.item {
max-width: 150px;
margin: 0;
width: -webkit-fill-available;
@media (min-width: 376px) {
margin: 0 auto 0px;
}
.card-title {
color: #4a68b9;
font-size: 10px;
margin-bottom: 4px;
font-size: 1rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-text {
font-size: 22px;
span {
font-size: 11px;
position: relative;
top: -2px;
}
}
&:last-child {
margin-bottom: 0;
}
.card-text span {
color: #ffffff66;
font-size: 12px;
top: 0px;
}
.fee-text{
border-bottom: 1px solid #ffffff1c;
color: #ffffff;
width: fit-content;
margin: auto;
line-height: 1.45;
padding: 0px 2px;
}
.fiat {
display: block;
font-size: 14px !important;
}
}
}
.loading-container{
min-height: 76px;
}
.card-text {
.skeleton-loader {
width: 100%;
display: block;
&:first-child {
max-width: 90px;
margin: 15px auto 3px;
}
&:last-child {
margin: 10px auto 3px;
max-width: 55px;
}
}
}

View File

@@ -0,0 +1,24 @@
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';
@Component({
selector: 'app-reserves-supply-stats',
templateUrl: './reserves-supply-stats.component.html',
styleUrls: ['./reserves-supply-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReservesSupplyStatsComponent implements OnInit {
@Input() currentReserves$: Observable<CurrentPegs>;
@Input() currentPeg$: Observable<CurrentPegs>;
env: Env;
constructor(private stateService: StateService) { }
ngOnInit(): void {
this.env = this.stateService.env;
}
}

View File

@@ -48,9 +48,16 @@
<div class="navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav {{ network.val }}">
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-home">
<a class="nav-link" [routerLink]="['/' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.dashboard" title="Dashboard"></fa-icon></a>
</li>
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-home" *ngIf="stateService.env.ACCELERATOR">
<a class="nav-link" [routerLink]="['/acceleration' | relativeUrl]" (click)="collapse()">
<fa-icon [icon]="['fas', 'rocket']" [fixedWidth]="true" i18n-title="master-page.accelerator-dashboard" title="Accelerator Dashboard"></fa-icon>
<span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
</a>
</li>
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD">
<a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="mining.mining-dashboard" title="Mining Dashboard"></fa-icon></a>
</li>

View File

@@ -29,6 +29,16 @@ li.nav-item {
padding-left: 8px;
padding-right: 8px;
}
@media (max-width: 429px) {
margin: auto 5px;
padding-left: 6px;
padding-right: 6px;
}
@media (max-width: 369px) {
margin: auto 3px;
padding-left: 4px;
padding-right: 4px;
}
}
@media (min-width: 992px) {
@@ -64,7 +74,10 @@ li.nav-item {
}
a {
font-size: 0.8em;
@media (min-width: 375px) {
@media (min-width: 370px) {
font-size: 0.9em;
}
@media (min-width: 430px) {
font-size: 1em;
}
}
@@ -198,7 +211,15 @@ nav {
margin: 24px 0px 0px -15px;
font-size: 8px;
@media (max-width: 767.98px) {
margin: 33px 0px 0px -19px;
margin: 30px 0px 0px -19px;
font-size: 7px;
}
@media (max-width: 3429px) {
margin: 25px 0px 0px -19px;
font-size: 7px;
}
@media (max-width: 369px) {
margin: 20px 0px 0px -19px;
font-size: 7px;
}
}

View File

@@ -1,11 +1,13 @@
<app-block-overview-graph
#blockGraph
[isLoading]="isLoading$ | async"
[resolution]="86"
[resolution]="resolution"
[blockLimit]="stateService.blockVSize"
[orientation]="timeLtr ? 'right' : 'left'"
[flip]="true"
[showFilters]="showFilters"
[filterFlags]="filterFlags"
[filterMode]="filterMode"
[overrideColors]="overrideColors"
(txClickEvent)="onTxClick($event)"
></app-block-overview-graph>

View File

@@ -3,13 +3,14 @@ import { Component, ComponentRef, ViewChild, HostListener, Input, Output, EventE
import { StateService } from '../../services/state.service';
import { MempoolBlockDelta, TransactionStripped } from '../../interfaces/websocket.interface';
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
import { Subscription, BehaviorSubject, merge, of } from 'rxjs';
import { switchMap, filter } from 'rxjs/operators';
import { Subscription, BehaviorSubject, merge, of, timer } from 'rxjs';
import { switchMap, filter, concatMap, map } from 'rxjs/operators';
import { WebsocketService } from '../../services/websocket.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { Router } from '@angular/router';
import { Color } from '../block-overview-graph/sprite-types';
import TxView from '../block-overview-graph/tx-view';
import { FilterMode } from '../../shared/filters.utils';
@Component({
selector: 'app-mempool-block-overview',
@@ -18,8 +19,11 @@ import TxView from '../block-overview-graph/tx-view';
})
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
@Input() index: number;
@Input() resolution = 86;
@Input() showFilters: boolean = false;
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
@Input() filterFlags: bigint | undefined = undefined;
@Input() filterMode: FilterMode = 'and';
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
@@ -33,7 +37,11 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
poolDirection: string = 'left';
blockSub: Subscription;
deltaSub: Subscription;
rateLimit = 1000;
private lastEventTime = Date.now() - this.rateLimit;
private subId = 0;
firstLoad: boolean = true;
constructor(
public stateService: StateService,
@@ -53,20 +61,82 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
ngAfterViewInit(): void {
this.blockSub = merge(
of(true),
this.stateService.connectionState$.pipe(filter((state) => state === 2))
)
.pipe(switchMap(() => this.stateService.mempoolBlockTransactions$))
.subscribe((transactionsStripped) => {
this.replaceBlock(transactionsStripped);
});
this.deltaSub = this.stateService.mempoolBlockDelta$.subscribe((delta) => {
this.updateBlock(delta);
this.stateService.mempoolBlockTransactions$,
this.stateService.mempoolBlockDelta$,
).pipe(
concatMap(update => {
const now = Date.now();
const timeSinceLastEvent = now - this.lastEventTime;
this.lastEventTime = Math.max(now, this.lastEventTime + this.rateLimit);
const subId = this.subId;
// If time since last event is less than X seconds, delay this event
if (timeSinceLastEvent < this.rateLimit) {
return timer(this.rateLimit - timeSinceLastEvent).pipe(
// Emit the event after the timer
map(() => ({ update, subId }))
);
} else {
// If enough time has passed, emit the event immediately
return of({ update, subId });
}
})
).subscribe(({ update, subId }) => {
// discard stale updates after a block transition
if (subId !== this.subId) {
return;
}
// process update
if (update['added']) {
// delta
this.updateBlock(update as MempoolBlockDelta);
} else {
const transactionsStripped = update as TransactionStripped[];
// new transactions
if (this.firstLoad) {
this.replaceBlock(transactionsStripped);
} else {
const inOldBlock = {};
const inNewBlock = {};
const added: TransactionStripped[] = [];
const changed: { txid: string, rate: number | undefined, flags: number, acc: boolean | undefined }[] = [];
const removed: string[] = [];
for (const tx of transactionsStripped) {
inNewBlock[tx.txid] = true;
}
for (const txid of Object.keys(this.blockGraph?.scene?.txs || {})) {
inOldBlock[txid] = true;
if (!inNewBlock[txid]) {
removed.push(txid);
}
}
for (const tx of transactionsStripped) {
if (!inOldBlock[tx.txid]) {
added.push(tx);
} else {
changed.push({
txid: tx.txid,
rate: tx.rate,
flags: tx.flags,
acc: tx.acc
});
}
}
this.updateBlock({
removed,
changed,
added
});
}
}
});
}
ngOnChanges(changes): void {
if (changes.index) {
this.subId++;
this.firstLoad = true;
if (this.blockGraph) {
this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection);
}
@@ -77,7 +147,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
ngOnDestroy(): void {
this.blockSub.unsubscribe();
this.deltaSub.unsubscribe();
this.timeLtrSubscription.unsubscribe();
this.websocketService.stopTrackMempoolBlock();
}

View File

@@ -64,6 +64,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
}),
tap(() => {
this.stateService.markBlock$.next({ mempoolBlockIndex: this.mempoolBlockIndex });
this.websocketService.startTrackMempoolBlock(this.mempoolBlockIndex);
})
);
@@ -74,6 +75,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.stateService.markBlock$.next({});
this.websocketService.stopTrackMempoolBlock();
}
getOrdinal(mempoolBlock: MempoolBlock): string {

View File

@@ -34,7 +34,7 @@
<app-time kind="until" [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
</ng-template>
<ng-template #timeDiffMainnet>
<app-time kind="until" [time]="da.timeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
<app-time kind="until" [time]="da.adjustedTimeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
</ng-template>
</div>
<ng-template #mergedBlock>

View File

@@ -2,8 +2,19 @@
<div class="d-flex menu-click">
<nav class="scrollable menu-click">
<span *ngIf="userAuth" class="menu-click">
<strong class="menu-click text-nowrap ellipsis">@ {{ userAuth.user.username }}</strong>
<span *ngIf="user$ | async as user" class="menu-click">
<span class="menu-click text-nowrap ellipsis">
<strong>
<span *ngIf="user.username.includes('@'); else usernamenospace">{{ user.username }}</span>
<ng-template #usernamenospace>@{{ user.username }}</ng-template>
</strong>
</span>
<span class="badge mr-1 badge-og" *ngIf="user.ogRank">
OG #{{ user.ogRank }}
</span>
<span class="badge mr-1 badge-default" [class]="'badge-' + user.subscription_tag" *ngIf="user.subscription_tag !== 'free'">
{{ user.subscription_tag.toUpperCase() }}
</span>
</span>
<a *ngIf="!userAuth" class="d-flex justify-content-center align-items-center nav-link m-0 menu-click" routerLink="/login" role="tab" (click)="onLinkClick('/login')">
<fa-icon class="menu-click" [icon]="['fas', 'user-circle']" [fixedWidth]="true" style="font-size: 25px;margin-right: 15px;"></fa-icon>

View File

@@ -55,4 +55,36 @@
@media screen and (max-height: 450px) {
.sidenav a {font-size: 18px;}
}
.badge-default {
background-color: black;
}
.badge-og {
background-color: #4a68b9;
}
.badge-pleb {
background-color: #3ccbe3;
}
.badge-chad {
background-color: #957d0b;
}
.badge-whale {
background-color: #653b9c;
}
.badge-silver {
background-color: #95a5a6;
}
.badge-gold {
background-color: #f1c40f;
}
.badge-platinum {
background-color: #653b9c;
}

View File

@@ -1,10 +1,10 @@
import { Component, OnInit, Input, Output, EventEmitter, HostListener, OnDestroy } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiService } from '../../services/api.service';
import { MenuGroup } from '../../interfaces/services.interface';
import { StorageService } from '../../services/storage.service';
import { Router, NavigationStart } from '@angular/router';
import { StateService } from '../../services/state.service';
import { IUser, ServicesApiServices } from '../../services/services-api.service';
@Component({
selector: 'app-menu',
@@ -18,11 +18,12 @@ export class MenuComponent implements OnInit, OnDestroy {
@Output() menuToggled = new EventEmitter<boolean>();
userMenuGroups$: Observable<MenuGroup[]> | undefined;
user$: Observable<IUser | null>;
userAuth: any | undefined;
isServicesPage = false;
constructor(
private apiService: ApiService,
private servicesApiServices: ServicesApiServices,
private storageService: StorageService,
private router: Router,
private stateService: StateService
@@ -32,7 +33,8 @@ export class MenuComponent implements OnInit, OnDestroy {
this.userAuth = this.storageService.getAuth();
if (this.stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE) {
this.userMenuGroups$ = this.apiService.getUserMenuGroups$();
this.userMenuGroups$ = this.servicesApiServices.getUserMenuGroups$();
this.user$ = this.servicesApiServices.userSubject$;
}
this.isServicesPage = this.router.url.includes('/services/');
@@ -55,10 +57,10 @@ export class MenuComponent implements OnInit, OnDestroy {
}
logout(): void {
this.apiService.logout$().subscribe(() => {
this.servicesApiServices.logout$().subscribe(() => {
this.loggedOut.emit(true);
if (this.stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE) {
this.userMenuGroups$ = this.apiService.getUserMenuGroups$();
this.userMenuGroups$ = this.servicesApiServices.getUserMenuGroups$();
this.router.navigateByUrl('/');
}
});

View File

@@ -26,9 +26,11 @@
<!-- pool distribution -->
<div class="col" style="margin-bottom: 1.47rem">
<div class="card graph-card">
<div class="card">
<div class="card-body pl-2 pr-2">
<app-pool-ranking [attr.data-cy]="'pool-distribution'" [widget]=true></app-pool-ranking>
<div class="mempool-graph">
<app-pool-ranking [height]="graphHeight" [attr.data-cy]="'pool-distribution'" [widget]=true></app-pool-ranking>
</div>
<div class="mt-1"><a [attr.data-cy]="'pool-distribution-view-more'" [routerLink]="['/graphs/mining/pools' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
@@ -38,7 +40,9 @@
<div class="col" style="margin-bottom: 1.47rem">
<div class="card">
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
<app-hashrate-chart [attr.data-cy]="'hashrate-graph'" [widget]="true"></app-hashrate-chart>
<div class="fixed-mempool-graph">
<app-hashrate-chart [height]="graphHeight" [attr.data-cy]="'hashrate-graph'" [widget]="true"></app-hashrate-chart>
</div>
<div class="mt-1"><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" fragment="1y" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>

View File

@@ -17,6 +17,19 @@
}
}
.fixed-mempool-graph {
height: 330px;
}
.mempool-graph, .fixed-mempool-graph {
@media (min-width: 768px) {
height: 345px;
}
@media (min-width: 992px) {
height: 440px;
}
}
.card-title {
font-size: 1rem;
color: #4a68b9;

View File

@@ -1,4 +1,4 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, OnInit } from '@angular/core';
import { SeoService } from '../../services/seo.service';
import { WebsocketService } from '../../services/websocket.service';
import { StateService } from '../../services/state.service';
@@ -11,6 +11,8 @@ import { EventType, NavigationStart, Router } from '@angular/router';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MiningDashboardComponent implements OnInit, AfterViewInit {
graphHeight = 375;
constructor(
private seoService: SeoService,
private websocketService: WebsocketService,
@@ -22,6 +24,7 @@ export class MiningDashboardComponent implements OnInit, AfterViewInit {
}
ngOnInit(): void {
this.onResize();
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
}
@@ -35,4 +38,15 @@ export class MiningDashboardComponent implements OnInit, AfterViewInit {
}
});
}
@HostListener('window:resize', ['$event'])
onResize(): void {
if (window.innerWidth >= 992) {
this.graphHeight = 335;
} else if (window.innerWidth >= 768) {
this.graphHeight = 245;
} else {
this.graphHeight = 240;
}
}
}

View File

@@ -76,7 +76,7 @@
</div>
<div [class]="!widget ? '' : 'pb-0'" class="container pb-lg-0">
<div [class]="widget ? 'chart-widget' : 'chart'" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div [class]="widget ? 'chart-widget' : 'chart'" [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
@@ -92,7 +92,7 @@
<th class="" i18n="mining.pool-name">Pool</th>
<th class="" *ngIf="this.miningWindowPreference === '24h'" i18n="mining.hashrate">Hashrate</th>
<th class="" i18n="master-page.blocks">Blocks</th>
<th *ngIf="auditAvailable" class="health text-right widget" i18n="latest-blocks.avg_health"
<th *ngIf="auditAvailable" class="health text-right widget" [ngClass]="{'health-column': this.miningWindowPreference === '24h'}" i18n="latest-blocks.avg_health"
i18n-ngbTooltip="latest-blocks.avg_health" ngbTooltip="Avg Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Avg Health</th>
<th *ngIf="auditAvailable" class="d-none d-sm-table-cell" i18n="mining.fees-per-block">Avg Block Fees</th>
<th class="d-none d-lg-table-cell" i18n="mining.empty-blocks">Empty Blocks</th>
@@ -104,13 +104,13 @@
<td class="text-right">
<img width="25" height="25" src="{{ pool.logo }}" [alt]="pool.name + ' mining pool logo'" onError="this.src = '/resources/mining-pools/default.svg'">
</td>
<td class=""><a [routerLink]="[('/mining/pool/' + pool.slug) | relativeUrl]">{{ pool.name }}</a></td>
<td class="pool-name"><a [routerLink]="[('/mining/pool/' + pool.slug) | relativeUrl]">{{ pool.name }}</a></td>
<td class="" *ngIf="this.miningWindowPreference === '24h'">{{ pool.lastEstimatedHashrate }} {{
miningStats.miningUnits.hashrateUnit }}</td>
<td class="d-flex justify-content-center">
{{ pool.blockCount }}<span class="d-none d-md-table-cell">&nbsp;({{ pool.share }}%)</span>
</td>
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable, 'health-column': this.miningWindowPreference === '24h'}">
<a
class="health-badge badge"
[class.badge-success]="pool.avgMatchRate >= 99"

View File

@@ -28,7 +28,9 @@
.chart-widget {
width: 100%;
height: 100%;
height: 240px;
@media (max-width: 767px) {
max-height: 240px;
}
@media (max-width: 485px) {
max-height: 200px;
}
@@ -41,6 +43,18 @@
}
}
@media (max-width: 430px) {
.pool-name {
max-width: 110px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.health-column {
display: none;
}
}
.loadingGraphs {
position: absolute;
top: 50%;

View File

@@ -20,6 +20,7 @@ import { isMobile } from '../../shared/common.utils';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PoolRankingComponent implements OnInit {
@Input() height: number = 300;
@Input() widget = false;
miningWindowPreference: string;
@@ -162,7 +163,7 @@ export class PoolRankingComponent implements OnInit {
const i = pool.blockCount.toString();
if (this.miningWindowPreference === '24h') {
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
pool.lastEstimatedHashrate.toString() + ' PH/s' +
pool.lastEstimatedHashrate.toString() + ' ' + miningStats.miningUnits.hashrateUnit +
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
} else {
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
@@ -200,7 +201,7 @@ export class PoolRankingComponent implements OnInit {
const i = totalBlockOther.toString();
if (this.miningWindowPreference === '24h') {
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
totalEstimatedHashrateOther.toString() + ' PH/s' +
totalEstimatedHashrateOther.toString() + ' ' + miningStats.miningUnits.hashrateUnit +
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
} else {
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +

View File

@@ -224,7 +224,7 @@
<a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a>
</td>
<td class="timestamp">
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
</td>
<td class="mined">
<app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time>

View File

@@ -115,13 +115,13 @@ export class PoolComponent implements OnInit {
prepareChartOptions(data) {
let title: object;
if (data.length === 0) {
if (data.length <= 1) {
title = {
textStyle: {
color: 'grey',
fontSize: 15
},
text: $localize`:@@23555386d8af1ff73f297e89dd4af3f4689fb9dd:Indexing blocks`,
text: $localize`Not enough data yet`,
left: 'center',
top: 'center'
};
@@ -172,14 +172,14 @@ export class PoolComponent implements OnInit {
`;
}.bind(this)
},
xAxis: data.length === 0 ? undefined : {
xAxis: data.length <= 1 ? undefined : {
type: 'time',
splitNumber: (this.isMobile()) ? 5 : 10,
axisLabel: {
hideOverlap: true,
}
},
yAxis: data.length === 0 ? undefined : [
yAxis: data.length <= 1 ? undefined : [
{
min: (value) => {
return value.min * 0.9;
@@ -198,7 +198,7 @@ export class PoolComponent implements OnInit {
}
},
],
series: data.length === 0 ? undefined : [
series: data.length <= 1 ? undefined : [
{
zlevel: 0,
name: 'Hashrate',
@@ -211,7 +211,7 @@ export class PoolComponent implements OnInit {
},
},
],
dataZoom: data.length === 0 ? undefined : [{
dataZoom: data.length <= 1 ? undefined : [{
type: 'inside',
realtime: true,
zoomLock: true,

View File

@@ -1,7 +1,7 @@
<div class="rbf-timeline box" [class.mined]="replacements.mined">
<div class="timeline-wrapper">
<div class="timeline" *ngFor="let timeline of rows">
<div class="intervals">
<div class="timeline" *ngFor="let timeline of rows; let j = index">
<div class="intervals" *ngIf="j < rowLimit || timelineExpanded">
<ng-container *ngFor="let cell of timeline; let i = index;">
<div class="node-spacer"></div>
<ng-container *ngIf="i < timeline.length - 1">
@@ -13,7 +13,7 @@
</ng-container>
</ng-container>
</div>
<div class="nodes">
<div class="nodes" *ngIf="j < rowLimit || timelineExpanded">
<ng-container *ngFor="let cell of timeline; let i = index;">
<ng-container *ngIf="cell.replacement?.tx; else nonNode">
<div class="node"
@@ -37,7 +37,7 @@
</ng-container>
<ng-template #nonNode>
<ng-container [ngSwitch]="cell.connector">
<div class="connector" [class.fullrbf]="cell.fullRbf" *ngSwitchCase="'pipe'"><div class="pipe" [class.fullrbf]="cell.fullRbf"></div></div>
<div class="connector" [class.fullrbf]="cell.fullRbf" *ngSwitchCase="'pipe'"><div class="pipe" [class.fullrbf]="cell.fullRbf" [class.last-pipe]="!timelineExpanded && j === rowLimit - 1"></div></div>
<div class="connector" *ngSwitchCase="'corner'"><div class="corner" [class.fullrbf]="cell.fullRbf"></div></div>
<div class="node-spacer" *ngSwitchDefault></div>
</ng-container>
@@ -51,6 +51,16 @@
</div>
</div>
</div>
<div [class.fade-out]="!timelineExpanded && rows.length > rowLimit"></div>
<div class="toggle-wrapper" *ngIf="rows.length > rowLimit && rowLimit !== 0">
<button class="btn btn-sm btn-primary graph-toggle" (click)="toggleTimeline(true);" *ngIf="!timelineExpanded; else collapseBtn">
<span i18n="show-all">Show all</span>
(<ng-container *ngTemplateOutlet="xRemaining; context: {$implicit: rows.length - rowLimit}"></ng-container>)
</button>
<ng-template #collapseBtn>
<button class="btn btn-sm btn-primary graph-toggle" (click)="toggleTimeline(false);"><span i18n="show-less">Show less</span></button>
</ng-template>
</div>
<ng-template #nodeSpacer>
<div class="node-spacer"></div>
@@ -72,3 +82,5 @@
[isConnector]="hoverConnector"
></app-rbf-timeline-tooltip> -->
</div>
<ng-template #xRemaining let-x i18n="x-remaining">{{ x }} remaining</ng-template>

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