Merge branch 'master' into orangesurf/trademark-updates-2
This commit is contained in:
7
frontend/.gitignore
vendored
7
frontend/.gitignore
vendored
@@ -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
|
||||
|
||||
|
||||
212
frontend/package-lock.json
generated
212
frontend/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 }
|
||||
];
|
||||
|
||||
|
||||
@@ -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$()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin: 68px auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#become-sponsor-container.account {
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.become-sponsor {
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
|
||||
@@ -405,7 +405,7 @@
|
||||
|
||||
<div class="copyright">
|
||||
<div class="title">
|
||||
Copyright © 2019-2023<br>
|
||||
Copyright © 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®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem™, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool logo, the mempool Square logo, the mempool 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 <https://mempool.space/trademark-policy>.
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -53,11 +53,6 @@
|
||||
padding-bottom: 55px;
|
||||
}
|
||||
}
|
||||
.chart-widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 290px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-bottom: 10px;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> </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 »</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> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]="'‎' + (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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</ng-template>
|
||||
<ng-template #default>
|
||||
‎{{ 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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
text-align: left;
|
||||
min-width: 320px;
|
||||
pointer-events: none;
|
||||
z-index: 11;
|
||||
|
||||
&.clickable {
|
||||
pointer-events: all;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -42,12 +42,12 @@
|
||||
<ng-container *ngIf="!isLoadingBlock; else skeletonRows">
|
||||
<tr>
|
||||
<td class="td-width" i18n="block.hash">Hash</td>
|
||||
<td>‎<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>‎<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]="'‎' + (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>
|
||||
|
||||
@@ -57,11 +57,6 @@
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
.info-link {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.difference {
|
||||
margin-left: 0.5em;
|
||||
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
<ng-template #emptyfees>
|
||||
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
|
||||
|
||||
<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">
|
||||
|
||||
<app-fee-rate unitClass=""></app-fee-rate>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-total-fees'" *ngIf="showMiningInfo"
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
‎{{ 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'">
|
||||
|
||||
@@ -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">—</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">
|
||||
|
||||
@@ -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;
|
||||
})
|
||||
|
||||
@@ -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">—</div>
|
||||
</ng-template>
|
||||
<div class="symbol">
|
||||
<span i18n="difficulty-box.previous">Previous</span>:
|
||||
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
|
||||
|
||||
@@ -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;
|
||||
})
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -57,8 +57,6 @@
|
||||
}
|
||||
.chart-widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 240px;
|
||||
}
|
||||
|
||||
.pool-distribution {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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> <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> <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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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">
|
||||
‎{{ 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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">
|
||||
‎{{ 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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> <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> <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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,6 @@
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 16px);
|
||||
z-index: 100;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> <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> <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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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('/');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 »</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 »</a></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"> ({{ 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"
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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>` +
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
<a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a>
|
||||
</td>
|
||||
<td class="timestamp">
|
||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
‎{{ 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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user