Merge branch 'master' into nymkappa/mega-branch

This commit is contained in:
nymkappa
2024-01-15 10:01:38 +01:00
80 changed files with 2793 additions and 1040 deletions

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ 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;
@@ -64,6 +65,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
constructor(
private servicesApiService: ServicesApiServices,
private storageService: StorageService,
private audioService: AudioService,
private cd: ChangeDetectorRef
) { }
@@ -187,6 +189,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
this.userBid
).subscribe({
next: () => {
this.audioService.playSound('ascend-chime-cartoon');
this.showSuccess = true;
this.scrollToPreviewWithTimeout('successAlert', 'center');
this.estimateSubscription.unsubscribe();
@@ -211,4 +214,4 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
onResize(): void {
this.isMobile = window.innerWidth <= 767.98;
}
}
}

View File

@@ -1,10 +1,10 @@
<div class="container-xl" style="min-height: 335px" [class.widget]="widget" [class.full-height]="!widget">
<div class="container-xl widget-container" [class.widget]="widget" [class.full-height]="!widget">
<h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Accelerations</h1>
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
<div class="clearfix"></div>
<div style="min-height: 295px" *ngIf="accelerationList$ | async as accelerations">
<div class="acceleration-list" *ngIf="accelerationList$ | async as accelerations">
<table *ngIf="!accelerations || accelerations.length; else noData" class="table table-borderless table-fixed">
<thead>
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>

View File

@@ -14,11 +14,24 @@
.container-xl.legacy {
max-width: 1140px;
}
.container-xl.widget-container {
min-height: 335px;
@media (max-width: 767px) {
min-height: auto;
}
}
.container {
max-width: 100%;
}
.acceleration-list {
min-height: 295px;
@media (max-width: 767px) {
min-height: auto;
}
}
tr, td, th {
border: 0px;
padding-top: 0.65rem !important;
@@ -51,34 +64,63 @@ tr, td, th {
.txid {
width: 25%;
@media (max-width: 1100px) {
padding-right: 10px;
}
@media (max-width: 875px) {
display: none;
}
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;
}
}
.fee {
width: 35%;
}
.block {
.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;
}
}
.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;
}
}
.time {
width: 25%;
}
.fee {
width: 35%;
@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%
}
@@ -122,4 +164,7 @@ tr, td, th {
flex-direction: row;
align-items: center;
justify-content: center;
@media (max-width: 767px) {
height: 100px;
}
}

View File

@@ -139,6 +139,9 @@
}
.list-card {
height: 410px;
@media (max-width: 767px) {
height: auto;
}
}
.mempool-block-wrapper {

View File

@@ -1,7 +1,11 @@
<div class="block-filters" [class.filters-active]="activeFilters.length > 0" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200">
<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;">

View File

@@ -20,7 +20,21 @@
margin-left: 0.5em;
}
.info-badges {
display: flex;
flex-direction: row;
align-items: center;
float: right;
&:hover, &:active {
text-decoration: none;
}
}
.menu-toggle {
width: 2em;
height: 2em;
padding: 0px 1px;
opacity: 0;
cursor: pointer;
color: white;

View File

@@ -121,6 +121,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} else {
this.scene.setColorFunction(this.overrideColors);
}
this.start();
}
ngOnDestroy(): void {

View File

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

View File

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

View File

@@ -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';
@@ -121,21 +121,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.apiService.getAccelerationHistory$({ blockHash: block.id }) : of([])
]);
}
),
)
.subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
.subscribe(([transactions, accelerations]) => {
this.strippedTransactions = transactions;
const acceleratedInBlock = {};
for (const acc of accelerations) {
acceleratedInBlock[acc.txid] = acc;
}
for (const tx of transactions) {
if (acceleratedInBlock[tx.txid]) {
tx.acc = true;
}
}
this.isLoadingOverview = false;
if (this.blockGraph) {
this.blockGraph.destroy();

View File

@@ -42,12 +42,12 @@
<ng-container *ngIf="!isLoadingBlock; else skeletonRows">
<tr>
<td class="td-width" i18n="block.hash">Hash</td>
<td>&lrm;<a [routerLink]="['/block/' | relativeUrl, block.id]" title="{{ block.id }}">{{ block.id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.id"></app-clipboard></td>
<td>&lrm;<a [routerLink]="['/block/' | relativeUrl, block.id]" title="{{ block.id }}">{{ block.id | shortenString : 13 }}</a> <app-clipboard [text]="block.id"></app-clipboard></td>
</tr>
<tr>
<td i18n="block.timestamp">Timestamp</td>
<td>
<app-timestamp [unixTime]="block.timestamp" [precision]="1" minUnit="minute"></app-timestamp>
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="block.timestamp" [precision]="1" minUnit="minute"></app-timestamp>
</td>
</tr>
<tr>
@@ -59,7 +59,7 @@
<td [innerHTML]="'&lrm;' + (block.weight | wuBytes: 2)"></td>
</tr>
<tr *ngIf="auditAvailable">
<td><ng-container i18n="latest-blocks.health">Health</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="what-is-block-health"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></td>
<td><ng-container i18n="latest-blocks.health">Health</ng-container>&nbsp;<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"
@@ -233,7 +233,9 @@
<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">
@@ -245,7 +247,9 @@
<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 +456,24 @@
</table>
</ng-template>
<ng-template #loadingDetailsSkeletons>
<table class="table table-borderless table-striped audit-details-table">
<tbody>
<tr>
<td class="w-50" i18n="block.total-fees|Total fees in a block">Total fees</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n="block.weight">Weight</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n="mempool-block.transactions">Transactions</td>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</ng-template>
<br>
<br>

View File

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

View File

@@ -328,17 +328,28 @@ export class BlockComponent implements OnInit, OnDestroy {
this.overviewError = err;
return of(null);
})
)
),
this.stateService.env.ACCELERATOR === true && block.height > 819500 ? this.apiService.getAccelerationHistory$({ blockHash: block.id }) : of([])
]);
})
)
.subscribe(([transactions, blockAudit]) => {
.subscribe(([transactions, blockAudit, accelerations]) => {
if (transactions) {
this.strippedTransactions = transactions;
} else {
this.strippedTransactions = [];
}
const acceleratedInBlock = {};
for (const acc of accelerations) {
acceleratedInBlock[acc.txid] = acc;
}
for (const tx of transactions) {
if (acceleratedInBlock[tx.txid]) {
tx.acc = true;
}
}
this.blockAudit = null;
if (transactions && blockAudit) {
const inTemplate = {};

View File

@@ -46,7 +46,7 @@
</div>
</td>
<td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
</td>
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
<a

View File

@@ -53,7 +53,7 @@
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['signet'] || '/signet')" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage + (networkPaths['bisq'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bisq" width="22" height="22" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
<a ngbDropdownItem *ngIf="env.BISQ_ENABLED" [href]="env.BISQ_WEBSITE_URL + urlLanguage + (networkPaths['bisq'] || '')" class="mainnet"><app-svg-images name="bisq" width="22" height="22" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
<a ngbDropdownItem class="liquid mr-1" [class.active]="network.val === 'liquid'" [routerLink]="networkPaths['liquid'] || '/'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
<a ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" [routerLink]="networkPaths['liquidtestnet'] || '/testnet'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
</div>
@@ -98,4 +98,4 @@
<app-global-footer *ngIf="footerVisible"></app-global-footer>
</ng-container>
</ng-container>

View File

@@ -3,8 +3,8 @@ 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';
@@ -33,7 +33,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 +57,81 @@ 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, 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,
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 +142,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
ngOnDestroy(): void {
this.blockSub.unsubscribe();
this.deltaSub.unsubscribe();
this.timeLtrSubscription.unsubscribe();
this.websocketService.stopTrackMempoolBlock();
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,12 +30,32 @@
overflow-x: auto;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.fade-out {
position: relative;
&::before {
content: '';
position: absolute;
width: 100%;
height: 70px;
top: -70px;
background: linear-gradient(to bottom, rgba(36, 39, 62, 0) 0%, rgba(36, 39, 62, 1) 100%);
z-index: 1;
}
}
.toggle-wrapper {
width: 100%;
text-align: center;
margin: 1.25em 0 0;
}
.intervals, .nodes {
min-width: 100%;
display: flex;
@@ -191,6 +211,10 @@
&.fullrbf {
border-right: solid 10px #1bd8f4;
}
&.last-pipe {
height: 150px;
bottom: -42px;
}
}
.corner {

View File

@@ -25,7 +25,9 @@ function isTimelineCell(val: RbfTree | TimelineCell): boolean {
export class RbfTimelineComponent implements OnInit, OnChanges {
@Input() replacements: RbfTree;
@Input() txid: string;
@Input() rowLimit: number = 5; // If explicitly set to 0, all timelines rows will be displayed by default
rows: TimelineCell[][] = [];
timelineExpanded: boolean = this.rowLimit === 0;
hoverInfo: RbfTree | null = null;
tooltipPosition = null;
@@ -191,6 +193,10 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
return rows;
}
toggleTimeline(expand: boolean): void {
this.timelineExpanded = expand;
}
scrollToSelected() {
const node = document.getElementById('node-' + this.txid);
if (node) {

View File

@@ -2,13 +2,14 @@ import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewC
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { EventType, NavigationStart, Router } from '@angular/router';
import { AssetsService } from '../../services/assets.service';
import { StateService } from '../../services/state.service';
import { Env, StateService } from '../../services/state.service';
import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap, catchError, map, startWith, tap } from 'rxjs/operators';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { ApiService } from '../../services/api.service';
import { SearchResultsComponent } from './search-results/search-results.component';
import { Network, findOtherNetworks, getRegex, getTargetUrl, needBaseModuleChange } from '../../shared/regex.utils';
@Component({
selector: 'app-search-form',
@@ -18,7 +19,7 @@ import { SearchResultsComponent } from './search-results/search-results.componen
})
export class SearchFormComponent implements OnInit {
@Input() hamburgerOpen = false;
env: Env;
network = '';
assets: object = {};
isSearching = false;
@@ -36,12 +37,13 @@ export class SearchFormComponent implements OnInit {
}
}
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64})$/;
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
regexBlockheight = /^[0-9]{1,9}$/;
regexDate = /^(?:\d{4}[-/]\d{1,2}[-/]\d{1,2}(?: \d{1,2}:\d{2})?)$/;
regexUnixTimestamp = /^\d{10}$/;
regexAddress = getRegex('address', 'mainnet'); // Default to mainnet
regexBlockhash = getRegex('blockhash', 'mainnet');
regexTransaction = getRegex('transaction');
regexBlockheight = getRegex('blockheight');
regexDate = getRegex('date');
regexUnixTimestamp = getRegex('timestamp');
focus$ = new Subject<string>();
click$ = new Subject<string>();
@@ -66,8 +68,14 @@ export class SearchFormComponent implements OnInit {
}
ngOnInit(): void {
this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.env = this.stateService.env;
this.stateService.networkChanged$.subscribe((network) => {
this.network = network;
// TODO: Eventually change network type here from string to enum of consts
this.regexAddress = getRegex('address', network as any || 'mainnet');
this.regexBlockhash = getRegex('blockhash', network as any || 'mainnet');
});
this.router.events.subscribe((e: NavigationStart) => { // Reset search focus when changing page
if (this.searchInput && e.type === EventType.NavigationStart) {
this.searchInput.nativeElement.blur();
@@ -96,9 +104,6 @@ export class SearchFormComponent implements OnInit {
const searchText$ = this.searchForm.get('searchText').valueChanges
.pipe(
map((text) => {
if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) {
return text.substr(1);
}
return text.trim();
}),
tap((text) => {
@@ -132,9 +137,6 @@ export class SearchFormComponent implements OnInit {
);
}),
map((result: any[]) => {
if (this.network === 'bisq') {
result[0] = result[0].map((address: string) => 'B' + address);
}
return result;
}),
tap(() => {
@@ -164,6 +166,7 @@ export class SearchFormComponent implements OnInit {
blockHeight: false,
txId: false,
address: false,
otherNetworks: [],
addresses: [],
nodes: [],
channels: [],
@@ -174,15 +177,21 @@ export class SearchFormComponent implements OnInit {
const addressPrefixSearchResults = result[0];
const lightningResults = result[1];
const matchesBlockHeight = this.regexBlockheight.test(searchText);
const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date';
const matchesUnixTimestamp = this.regexUnixTimestamp.test(searchText);
// Do not show date and timestamp results for liquid and bisq
const isNetworkBitcoin = this.network === '' || this.network === 'testnet' || this.network === 'signet';
const matchesBlockHeight = this.regexBlockheight.test(searchText) && parseInt(searchText) <= this.stateService.latestBlockHeight;
const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date' && new Date(searchText).getTime() <= Date.now() && isNetworkBitcoin;
const matchesUnixTimestamp = this.regexUnixTimestamp.test(searchText) && parseInt(searchText) <= Math.floor(Date.now() / 1000) && isNetworkBitcoin;
const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText);
const matchesBlockHash = this.regexBlockhash.test(searchText);
const matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
let matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
const otherNetworks = findOtherNetworks(searchText, this.network as any || 'mainnet', this.env);
if (matchesAddress && this.network === 'bisq') {
searchText = 'B' + searchText;
// Add B prefix to addresses in Bisq network
if (!matchesAddress && this.network === 'bisq' && getRegex('address', 'mainnet').test(searchText)) {
searchText = 'B' + searchText;
matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
}
if (matchesDateTime && searchText.indexOf('/') !== -1) {
@@ -198,7 +207,8 @@ export class SearchFormComponent implements OnInit {
txId: matchesTxId,
blockHash: matchesBlockHash,
address: matchesAddress,
addresses: addressPrefixSearchResults,
addresses: matchesAddress && addressPrefixSearchResults.length === 1 && searchText === addressPrefixSearchResults[0] ? [] : addressPrefixSearchResults, // If there is only one address and it matches the search text, don't show it in the dropdown
otherNetworks: otherNetworks,
nodes: lightningResults.nodes,
channels: lightningResults.channels,
};
@@ -217,12 +227,21 @@ export class SearchFormComponent implements OnInit {
selectedResult(result: any): void {
if (typeof result === 'string') {
this.search(result);
} else if (typeof result === 'number') {
} else if (typeof result === 'number' && result <= this.stateService.latestBlockHeight) {
this.navigate('/block/', result.toString());
} else if (result.alias) {
this.navigate('/lightning/node/', result.public_key);
} else if (result.short_id) {
this.navigate('/lightning/channel/', result.id);
} else if (result.network) {
if (result.isNetworkAvailable) {
this.navigate('/address/', result.address, undefined, result.network);
} else {
this.searchForm.setValue({
searchText: '',
});
this.isSearching = false;
}
}
}
@@ -230,10 +249,13 @@ export class SearchFormComponent implements OnInit {
const searchText = result || this.searchForm.value.searchText.trim();
if (searchText) {
this.isSearching = true;
if (!this.regexTransaction.test(searchText) && this.regexAddress.test(searchText)) {
this.navigate('/address/', searchText);
} else if (this.regexBlockhash.test(searchText) || this.regexBlockheight.test(searchText)) {
} else if (this.regexBlockhash.test(searchText)) {
this.navigate('/block/', searchText);
} else if (this.regexBlockheight.test(searchText)) {
parseInt(searchText) <= this.stateService.latestBlockHeight ? this.navigate('/block/', searchText) : this.isSearching = false;
} else if (this.regexTransaction.test(searchText)) {
const matches = this.regexTransaction.exec(searchText);
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
@@ -256,6 +278,11 @@ export class SearchFormComponent implements OnInit {
} else if (this.regexDate.test(searchText) || this.regexUnixTimestamp.test(searchText)) {
let timestamp: number;
this.regexDate.test(searchText) ? timestamp = Math.floor(new Date(searchText).getTime() / 1000) : timestamp = searchText;
// Check if timestamp is too far in the future or before the genesis block
if (timestamp > Math.floor(Date.now() / 1000)) {
this.isSearching = false;
return;
}
this.apiService.getBlockDataFromTimestamp$(timestamp).subscribe(
(data) => { this.navigate('/block/', data.hash); },
(error) => { console.log(error); this.isSearching = false; }
@@ -267,12 +294,17 @@ export class SearchFormComponent implements OnInit {
}
}
navigate(url: string, searchText: string, extras?: any): void {
this.router.navigate([this.relativeUrlPipe.transform(url), searchText], extras);
this.searchTriggered.emit();
this.searchForm.setValue({
searchText: '',
});
this.isSearching = false;
navigate(url: string, searchText: string, extras?: any, swapNetwork?: string) {
if (needBaseModuleChange(this.env.BASE_MODULE as 'liquid' | 'bisq' | 'mempool', swapNetwork as Network)) {
window.location.href = getTargetUrl(swapNetwork as Network, searchText, this.env);
} else {
this.router.navigate([this.relativeUrlPipe.transform(url, swapNetwork), searchText], extras);
this.searchTriggered.emit();
this.searchForm.setValue({
searchText: '',
});
this.isSearching = false;
}
}
}

View File

@@ -1,4 +1,4 @@
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.hashQuickMatch && !results.addresses.length && !results.nodes.length && !results.channels.length">
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.hashQuickMatch && !results.otherNetworks.length && !results.addresses.length && !results.nodes.length && !results.channels.length">
<ng-template [ngIf]="results.blockHeight">
<div class="card-title" i18n="search.bitcoin-block-height">Bitcoin Block Height</div>
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
@@ -35,10 +35,18 @@
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : 13 }"></ng-container>
</button>
</ng-template>
<ng-template [ngIf]="results.otherNetworks.length">
<div class="card-title danger" i18n="search.other-networks">Other Network Address</div>
<ng-template ngFor [ngForOf]="results.otherNetworks" let-otherNetwork let-i="index">
<button (click)="clickItem(results.hashQuickMatch + i)" [class.active]="(results.hashQuickMatch + i) === activeIdx" [class.inactive]="!otherNetwork.isNetworkAvailable" type="button" role="option" class="dropdown-item">
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: otherNetwork.address| shortenString : isMobile ? 20 : 25 }"></ng-container>&nbsp;<b>({{ otherNetwork.network.charAt(0).toUpperCase() + otherNetwork.network.slice(1) }})</b>
</button>
</ng-template>
</ng-template>
<ng-template [ngIf]="results.addresses.length">
<div class="card-title" i18n="search.bitcoin-addresses">Bitcoin Addresses</div>
<ng-template ngFor [ngForOf]="results.addresses" let-address let-i="index">
<button (click)="clickItem(results.hashQuickMatch + i)" [class.active]="(results.hashQuickMatch + i) === activeIdx" type="button" role="option" class="dropdown-item">
<button (click)="clickItem(results.hashQuickMatch + results.otherNetworks.length + i)" [class.active]="(results.hashQuickMatch + results.otherNetworks.length + i) === activeIdx" type="button" role="option" class="dropdown-item">
<ngb-highlight [result]="address | shortenString : isMobile ? 25 : 36" [term]="results.searchText"></ngb-highlight>
</button>
</ng-template>
@@ -46,7 +54,7 @@
<ng-template [ngIf]="results.nodes.length">
<div class="card-title" i18n="search.lightning-nodes">Lightning Nodes</div>
<ng-template ngFor [ngForOf]="results.nodes" let-node let-i="index">
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.hashQuickMatch + results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item">
<button (click)="clickItem(results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item">
<ngb-highlight [result]="node.alias" [term]="results.searchText"></ngb-highlight> &nbsp;<span class="symbol">{{ node.public_key | shortenString : 10 }}</span>
</button>
</ng-template>
@@ -54,7 +62,7 @@
<ng-template [ngIf]="results.channels.length">
<div class="card-title" i18n="search.lightning-channels">Lightning Channels</div>
<ng-template ngFor [ngForOf]="results.channels" let-channel let-i="index">
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2" [class.active]="results.hashQuickMatch + results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
<button (click)="clickItem(results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2" [class.active]="results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
<ngb-highlight [result]="channel.short_id" [term]="results.searchText"></ngb-highlight> &nbsp;<span class="symbol">{{ channel.id }}</span>
</button>
</ng-template>

View File

@@ -7,6 +7,10 @@
margin-left: 10px;
}
.danger {
color: #dc3545;
}
.dropdown-menu {
position: absolute;
top: 42px;

View File

@@ -22,7 +22,7 @@ export class SearchResultsComponent implements OnChanges {
ngOnChanges() {
this.activeIdx = 0;
if (this.results) {
this.resultsFlattened = [...(this.results.hashQuickMatch ? [this.results.searchText] : []), ...this.results.addresses, ...this.results.nodes, ...this.results.channels];
this.resultsFlattened = [...(this.results.hashQuickMatch ? [this.results.searchText] : []), ...this.results.otherNetworks, ...this.results.addresses, ...this.results.nodes, ...this.results.channels];
}
}
@@ -45,6 +45,9 @@ export class SearchResultsComponent implements OnChanges {
break;
case 'Enter':
event.preventDefault();
if (this.resultsFlattened[this.activeIdx]?.isNetworkAvailable === false) {
return;
}
if (this.resultsFlattened[this.activeIdx]) {
this.selectedResult.emit(this.resultsFlattened[this.activeIdx]);
} else {

View File

@@ -84,6 +84,14 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M9 9.5v-2a3 3 0 116 0v2c0 1.11-.603 2.08-1.5 2.599v1.224a1 1 0 00.629.928l2.05.82A3.693 3.693 0 0118.5 18.5h-13c0-1.51.92-2.868 2.321-3.428l2.05-.82a1 1 0 00.629-.929v-1.224A2.999 2.999 0 019 9.5z"></path>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'goggles'">
<svg viewBox="0 0 558.56415 255.62396" [attr.width]="width" [attr.height]="height" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="m 466.62029,0.15290693 c 2.84,0 5.90333,0.296667 9.19,0.88999997 17.05333,3.066667 31.92333,10.4666671 44.61,22.2000001 15.02,13.88 24.94,35.04 25.46,54.77 0.0133,0.54 0.26,0.93 0.74,1.17 5.05,2.52 9.14,6.28 10.82,11.39 0.79333,2.38667 1.16667,6.12 1.12,11.200003 -0.14,15.94667 -0.19,30.22667 -0.15,42.84 0.03,8.92 -3.88,14.6 -11.36,19.02 -0.71869,0.42495 -1.17676,1.1834 -1.22,2.02 -0.33,6.26 -0.72,13.09 -2.3,19.16 -2.2,8.49 -5.1,16.06 -9.74,23.78 -1.71333,2.85333 -3.41333,5.77333 -5.1,8.76 -2.64,4.68 -4.99,8.03667 -7.05,10.07 -2.45,2.43 -4.45,5.13 -7.02,7.38 -11.13,9.75 -26.36,16.58 -40.92,19.39 -5.05333,0.98 -11.52667,1.45667 -19.42,1.43 -25.96,-0.0667 -51.90333,-0.12 -77.83,-0.16 -10.08,-0.01 -20.45,-1.66 -29.9,-6 -15.07333,-6.92 -26.44667,-17.19667 -34.12,-30.83 -3.17,-5.64 -5.15,-11.78 -8.42,-17.3 -4.07333,-6.87333 -10.09,-11.46 -18.05,-13.76 -12.38,-3.57 -25.31,2.57 -32.13,13.15 -2.71,4.19 -4.3,9.04 -6.77,13.41 -1.99,3.52 -3.53,7.35 -5.7,10.78 -3.36667,5.34667 -5.54,8.46667 -6.52,9.36 -1.70667,1.56667 -3.5,3.16333 -5.38,4.79 -7.94,6.88 -18.64,11.02 -30.11,14.12 -5.35333,1.44667 -10.01667,2.18333 -13.99,2.21 -17.9,0.13333 -42.16667,0.17667 -72.8,0.13 -6.71333,-0.007 -13.376669,-0.14667 -19.990002,-0.42 -9.086667,-0.36667 -18.55,-2.88667 -28.39,-7.56 -18.233333,-8.65333 -32.26,-22.45333 -42.08,-41.4 -6.913333,-13.34667 -9.993333,-26.95 -9.24,-40.81 0.03144,-0.59686 -0.306835,-1.15004 -0.85,-1.39 -8.8400001,-3.98 -12.00000011,-10.78 -11.96000011,-20.36 0.05333333,-16.54667 0.03666667,-32.38 -0.0500000043,-47.500003 -0.0399999957,-7.54 4.09000001427,-13.94 11.34000011427,-16.48 0.543871,-0.19451 0.915559,-0.69668 0.94,-1.27 1.78,-39.54 32.94,-72.5000001 71.86,-77.57000007 4.18,-0.54 10.406667,-0.793333 18.680002,-0.76 363.8,0.15 0,0 363.8,0.15 z m 1.98,216.71000307 c 11.01,-2.05 20.88,-8.4 27.78,-17.23 7.51,-9.63 13.42,-21.1 13.47,-33.01 0.12,-25.71333 0.21667,-52.20667 0.29,-79.480003 0.0133,-6.34 -0.47333,-11.32 -1.46,-14.94 -3,-11 -9.03667,-19.66333 -18.11,-25.99 -7.32,-5.113333 -14.88667,-7.693333 -22.7,-7.74 -41.5,-0.233333 -85.66667,-0.276667 -132.5,-0.13 -62.12,0.206667 -142.78667,0.23 -242.000002,0.07 -12.52,-0.02 -23.406667,4.326667 -32.66,13.04 -7.79,7.34 -12.17,18.06 -12.13,28.98 0.09333,27.086673 0.06,54.470003 -0.1,82.150003 -0.03333,5.97333 0.393333,10.85333 1.28,14.64 4.28,18.33 19.71,34.9 37.96,39.18 3.7,0.86667 8.936667,1.27333 15.710002,1.22 24.08667,-0.18667 50.07,-0.19 77.95,-0.01 2.99333,0.02 5.98667,-0.41 8.98,-1.29 16.42,-4.84 24.52,-15.8 31.08,-30.65 1.76667,-3.99333 4.16333,-8.27 7.19,-12.83 7.95,-11.98 19.14,-18.59 32.66,-22.45 6.32667,-1.80667 11.84667,-2.70333 16.56,-2.69 25.87,0.07 47.99,12.39 58.26,35.87 1.54,3.52667 3.25333,7.07333 5.14,10.64 6.20667,11.73333 15.25333,19.07333 27.14,22.02 3.79333,0.94667 10.03667,1.39333 18.73,1.34 23.82667,-0.13333 47.73,-0.13667 71.71,-0.01 3.92,0.02 7.17667,-0.21333 9.77,-0.7 z" id="outline" />
<path fill="currentColor" opacity="0.3" d="m 496.97029,199.03291 c -6.9,8.83 -16.77,15.18 -27.78,17.23 q -3.89,0.73 -9.77,0.7 -35.97,-0.19 -71.71,0.01 -13.04,0.08 -18.73,-1.34 -17.83,-4.42 -27.14,-22.02 -2.83,-5.35 -5.14,-10.64 c -10.27,-23.48 -32.39,-35.8 -58.26,-35.87 q -7.07,-0.02 -16.56,2.69 c -13.52,3.86 -24.71,10.47 -32.66,22.45 q -4.54,6.84 -7.19,12.83 c -6.56,14.85 -14.66,25.81 -31.08,30.65 q -4.49,1.32 -8.98,1.29 -41.82,-0.27 -77.95,0.01 -10.160002,0.08 -15.710002,-1.22 c -18.25,-4.28 -33.68,-20.85 -37.96,-39.18 q -1.33,-5.68 -1.28,-14.64 0.24,-41.52 0.1,-82.150003 c -0.04,-10.92 4.34,-21.64 12.13,-28.98 q 13.88,-13.07 32.66,-13.04 148.820002,0.24 242.000002,-0.07 70.25,-0.22 132.5,0.13 11.72,0.07 22.7,7.74 13.61,9.49 18.11,25.99 1.48,5.43 1.46,14.94 -0.11,40.910003 -0.29,79.480003 c -0.05,11.91 -5.96,23.38 -13.47,33.01 z m -8.14,-101.340003 c 5.11,-2.24 9.54,-9.21 6.39,-14.8 q -1.59,-2.82 -4.29,-5.41 -8.04,-7.73 -15.91,-15.96 -2.88,-3.02 -5.51,-4.19 c -6.41,-2.84 -13.19,1.02 -15.6,7.25 -1.35,3.51 0.64,7.36 3.07,9.77 q 9.48,9.38 20.18,20.59 5.3,5.550003 11.67,2.75 z m -404.320002,-6.02 c 4.77,-6.16 10.61,-11.82 16.350002,-17.36 q 4.6,-4.45 3.41,-9.38 c -1.57,-6.47 -9.240002,-9.94 -15.220002,-7.57 q -2.72,1.07 -7.3,5.87 -7.54,7.9 -15.2,15.77 -2.83,2.9 -3.54,6.23 c -1.67,7.85 5.38,14.06 12.94,13.26 3.57,-0.39 6.5,-4.17 8.56,-6.82 z" id="lens" />
<path fill="currentColor" d="m 488.83029,97.692907 q -6.37,2.800003 -11.67,-2.75 -10.7,-11.21 -20.18,-20.59 c -2.43,-2.41 -4.42,-6.26 -3.07,-9.77 2.41,-6.23 9.19,-10.09 15.6,-7.25 q 2.63,1.17 5.51,4.19 7.87,8.23 15.91,15.96 2.7,2.59 4.29,5.41 c 3.15,5.59 -1.28,12.56 -6.39,14.8 z" id="glint-a" />
<path fill="currentColor" d="m 84.510288,91.672907 c -2.06,2.65 -4.99,6.43 -8.56,6.82 -7.56,0.8 -14.61,-5.41 -12.94,-13.26 q 0.71,-3.33 3.54,-6.23 7.66,-7.87 15.2,-15.77 4.58,-4.8 7.3,-5.87 c 5.98,-2.37 13.650002,1.1 15.220002,7.57 q 1.19,4.93 -3.41,9.38 c -5.740002,5.54 -11.580002,11.2 -16.350002,17.36 z" id="glint-b" />
</svg>
</ng-container>
</ng-container>
<ng-template #bitcoinLogo let-color let-width="width" let-height="height" let-viewBox="viewBox">

View File

@@ -62,6 +62,7 @@
<tr><td>mempool.space</td></tr>
<tr><td>Be your own explorer</td></tr>
<tr><td>Explore the full Bitcoin ecosystem</td></tr>
<tr><td>Mempool Goggles</td></tr>
</tbody>
</table>
</div>
@@ -314,7 +315,7 @@
<p>Also, if you are using our Marks in a way described in the sections "Uses for Which We Are Granting a License," you must include the following trademark attribution at the foot of the webpage where you have used the Mark (or, if in a book, on the credits page), on any packaging or labeling, and on advertising or marketing materials:</p>
<p>"The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&trade;, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein."</p>
<p>"The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&trade;, Mempool Goggles&trade;, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein."</p>
<li>What to Do When You See Abuse</li>
<br>

View File

@@ -299,7 +299,11 @@
<td [innerHTML]="'&lrm;' + (tx.weight / 4 | vbytes: 2)"></td>
</tr>
<tr *ngIf="adjustedVsize != null">
<td i18n="transaction.adjusted-vsize|Transaction Adjusted VSize">Adjusted vsize</td>
<td i18n="transaction.adjusted-vsize|Transaction Adjusted VSize">Adjusted vsize
<a class="info-link" [routerLink]="['/docs/faq/' | relativeUrl]" fragment="what-is-adjusted-vsize">
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
</a>
</td>
<td [innerHTML]="'&lrm;' + (adjustedVsize | vbytes: 2)"></td>
</tr>
<tr>
@@ -321,7 +325,11 @@
<td [innerHTML]="'&lrm;' + (tx.locktime | number)"></td>
</tr>
<tr *ngIf="sigops != null">
<td i18n="transaction.sigops|Transaction Sigops">Sigops</td>
<td i18n="transaction.sigops|Transaction Sigops">Sigops
<a class="info-link" [routerLink]="['/docs/faq/' | relativeUrl]" fragment="what-are-sigops">
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
</a>
</td>
<td [innerHTML]="'&lrm;' + (sigops | number)"></td>
</tr>
<tr>

View File

@@ -243,6 +243,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
});
this.fetchAccelerationSubscription = this.fetchAcceleration$.pipe(
filter(() => this.stateService.env.ACCELERATOR === true),
tap(() => {
this.accelerationInfo = null;
}),
@@ -440,7 +441,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
block_time: block.timestamp,
};
this.stateService.markBlock$.next({ blockHeight: block.height });
this.audioService.playSound('magic');
if (this.tx.acceleration || (this.accelerationInfo && ['accelerating', 'mined', 'completed'].includes(this.accelerationInfo.status))) {
this.audioService.playSound('wind-chimes-harp-ascend');
} else {
this.audioService.playSound('magic');
}
this.fetchAcceleration$.next(block.id);
}
});

View File

@@ -87,8 +87,8 @@
<th class="table-cell-new-fee" i18n="dashboard.new-transaction-fee">New fee</th>
<th class="table-cell-badges" i18n="transaction.status|Transaction Status">Status</th>
</thead>
<tbody>
<tr *ngFor="let replacement of replacements$ | async;">
<tbody *ngIf="replacements$ | async as replacements; else replacementsSkeleton">
<tr *ngFor="let replacement of replacements">
<td class="table-cell-txid">
<a [routerLink]="['/tx' | relativeUrl, replacement.txid]">
<app-truncate [text]="replacement.txid" [lastChars]="5"></app-truncate>
@@ -158,8 +158,8 @@
<th class="table-cell-fiat" *ngIf="(network$ | async) === ''">{{ currency }}</th>
<th class="table-cell-fees" i18n="transaction.fee|Transaction fee">Fee</th>
</thead>
<tbody>
<tr *ngFor="let transaction of transactions$ | async; let i = index;">
<tbody *ngIf="transactions$ | async as transactions else recentTransactionsSkeleton">
<tr *ngFor="let transaction of transactions; let i = index;">
<td class="table-cell-txid">
<a [routerLink]="['/tx' | relativeUrl, transaction.txid]">
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
@@ -199,6 +199,28 @@
</table>
</ng-template>
<ng-template #replacementsSkeleton>
<tbody>
<tr *ngFor="let i of [1,2,3,4,5,6]">
<td class="table-cell-txid"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
<td class="table-cell-old-fee"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
<td class="table-cell-new-fee"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
<td class="table-cell-badges"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
</tr>
</tbody>
</ng-template>
<ng-template #recentTransactionsSkeleton>
<tbody>
<tr *ngFor="let i of [1,2,3,4,5,6]">
<td class="table-cell-txid"><div class="skeleton-loader skeleton-loader-transactions"></div> </td>
<td class="table-cell-satoshis"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
<td class="table-cell-fiat" *ngIf="(network$ | async) === ''"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
<td class="table-cell-fees"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
</tr>
</tbody>
</ng-template>
<ng-template #loadingTransactions>
<div class="skeleton-loader skeleton-loader-transactions"></div>
</ng-template>

File diff suppressed because it is too large Load Diff

View File

@@ -279,12 +279,123 @@
<p class='note'>Because of this feature's resource usage and availability requirements, it is only supported on official mempool.space instances.</p>
</ng-template>
<ng-template type="how-do-mempool-goggles-work">
<p>Mempool Goggles are a set of filters that can be applied to the <a [routerLink]="['/mempool-block/0' | relativeUrl]">mempool block visualizations</a> to highlight different types of transactions.</p>
<p>There are currently 25 different Mempool Goggles filters, grouped into six categories:</p>
<dl>
<dt>Features</dt>
<dd>
<dl>
<dt>RBF enabled</dt>
<dd>The transaction opts-in to BIP-125 replaceability.</dd>
<dt>RBF disabled</dt>
<dd>The transaction does not opt-in to BIP-125 replaceability.</dd>
<dt>Version 1</dt>
<dd>The default version for most transactions.</dd>
<dt>Version 2</dt>
<dd>Required for transactions which use OP_CHECKSEQUENCEVERIFY relative timelocks.</dd>
</dl>
</dd>
<dt>Address Types</dt>
<dd>
<dl>
<dt>P2PK</dt>
<dd>Pay-to-public-key. A legacy output format most commonly found in old coinbase transactions.</dd>
<dt>Bare multisig</dt>
<dd>A legacy form of multisig, most commonly used for data embedding schemes (see also "Fake pubkey").</dd>
<dt>P2PKH</dt>
<dd>Pay-to-public-key-hash. A legacy address type that locks outputs to a public key.</dd>
<dt>P2SH</dt>
<dd>Pay-to-script-hash. A legacy address type that locks outputs to a <em>redeem script</em>.</dd>
<dt>P2WPKH</dt>
<dd>Pay-to-witness-public-key-hash. The SegWit version of P2PKH.</dd>
<dt>P2WSH</dt>
<dd>Pay-to-witness-script-hash. The SegWit version of P2SH.</dd>
<dt>Taproot</dt>
<dd>Addresses using the SegWit V1 format added in the Taproot upgrade.</dd>
</dl>
</dd>
<dt>Behavior</dt>
<dd>
<dl>
<dt>Paid for by child</dt>
<dd>The transaction's effective fee rate has been increased by a higher rate CPFP child.</dd>
<dt>Pays for parent</dt>
<dd>The transaction bumps the effective fee rate of a lower rate CPFP ancestor.</dd>
<dt>Replacement</dt>
<dd>The transaction replaced a prior version via RBF.</dd>
</dl>
</dd>
<dt>Data</dt>
<dd>
Different methods of embedding arbitrary data in a Bitcoin transaction.
<dl>
<dt>OP_RETURN</dt>
<dt>Fake pubkey</dt>
<dd>Data may be embedded in an invalid public key in a P2PK or Bare multisig output. This is a heuristic filter and can be prone to false positives and false negatives.</dd>
<dt>Inscription</dt>
<dd>Data is embedded in the witness script of a taproot input.</dd>
</dl>
</dd>
<dt>Heuristics</dt>
<dd>
These filters match common types of transactions according to subjective criteria.
<dl>
<dt>Coinjoin</dt>
<dd>A type of collaborative privacy-improving transaction.</dd>
<dt>Consolidation</dt>
<dd>The transaction condenses many inputs into a few outputs.</dd>
<dt>Batch payment</dt>
<dd>The transaction sends coins from a few inputs to many outputs.</dd>
</dl>
</dd>
<dt>Sighash Flags</dt>
<dd>
Different ways of signing inputs to Bitcoin transactions. Note that selecting multiple sighash filters will highlight transactions in which each sighash flag is used, but not necessarily in the same input.
<dl>
<dt>sighash_all</dt>
<dt>sighash_none</dt>
<dt>sighash_single</dt>
<dt>sighash_default</dt>
<dt>sighash_anyonecanpay</dt>
</dl>
</dd>
</dl>
</ng-template>
<ng-template type="what-are-sigops">
<p>A "sigop" is a way of accounting for the cost of "signature operations" in Bitcoin script, like <code>OP_CHECKSIG</code>, <code>OP_CHECKSIGVERIFY</code>, <code>OP_CHECKMULTISIG</code> and <code>OP_CHECKMULTISIGVERIFY</code></p>
<p>These signature operations incur different costs depending on whether they are single or multi-sig operations, and on where they appear in a Bitcoin transaction.</p>
<p>By consensus, each Bitcoin block is permitted to include a maximum of 80,000 sigops.</p>
</ng-template>
<ng-template type="what-is-adjusted-vsize">
<p>Bitcoin blocks have two independent consensus-enforced resource constraints - a 4MWU weight limit, and the 80,000 sigop limit.</p>
<p>Most transactions use a more of the weight limit than the sigop limit. However, some transactions use a disproportionate number of sigops compared to their weight.</p>
<p>To account for this, Bitcoin Core calculates and uses an "adjusted vsize" equal 5 times the number of sigops, or the unadjusted vsize, whichever is larger.</p>
<p>Then, during block template construction, Bitcoin Core selects transactions in descending order of fee rate measured in satoshis per <i>adjusted vsize</i></p>
<p>On mempool.space, effective fee rates for unconfirmed transactions are also measured in terms of satoshis per adjusted vsize, after accounting for CPFP relationships and other dependencies.</p>
</ng-template>
<ng-template type="why-do-the-projected-block-fee-ranges-overlap">
<p>The projected mempool blocks represent what we expect the next blocks would look like if they were mined right now, and so each projected block follows all of the same rules and constraints as real mined blocks.</p>
<p>Those constraints can sometimes cause transactions with lower fee rates to be included "ahead" of transactions with higher rates.</p>
<p>For example, if one projected block has a very small amount of space left, it might be able to fit one more tiny low fee rate transaction, while larger higher fee rate transactions have to wait for the following block.</p>
<p>A similar effect can occur when there are a large number of transactions with very many sigops. In that scenario, each projected block can only include up to 80,000 sigops worth of transactions, after which the remaining space can only be filled by potentially much lower fee transactions with zero sigops.</p>
<p>In extreme cases this can produce several projected blocks in a row with overlapping fee ranges, as a result of each projected block containing both high-feerate high-sigop transactions and lower feerate zero-sigop transactions.</p>
</ng-template>
<ng-template type="who-runs-this-website">
The official mempool.space website is operated by The Mempool Open Source Project. See more information on our <a [routerLink]="['/about']">About page</a>. There are also many unofficial instances of this website operated by individual members of the Bitcoin community.
</ng-template>
<ng-template type="host-my-own-instance-raspberry-pi">
We support one-click installation on a number of Raspberry Pi full-node distros including Umbrel, RaspiBlitz, MyNode, RoninDojo, and Start9's Embassy.
We support one-click installation on a number of Raspberry Pi full-node distros including Umbrel, RaspiBlitz, MyNode, RoninDojo, and StartOS.
</ng-template>
<ng-template type="host-my-own-instance-server">

View File

@@ -157,7 +157,7 @@ ul.no-bull.block-audit code{
position: fixed;
top: 80px;
overflow-y: auto;
height: calc(100vh - 50px);
height: calc(100vh - 75px);
scrollbar-color: #2d3348 #11131f;
scrollbar-width: thin;
}
@@ -389,3 +389,44 @@ h3 {
margin-bottom: 4rem;
}
}
/* styles for nested definition lists */
dl {
margin: 0;
padding: 0;
}
dt {
font-weight: bold;
color: #4a68b9;
padding: 5px 0;
}
dd {
padding: 2px 0;
& > dl {
padding-left: 1em;
border-left: 2px solid #4a68b9;
margin-left: 1em;
margin-top: 5px;
}
& > dl > dt {
display: inline;
font-weight: normal;
color: #e83e8c;
font-family: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New;
text-transform: uppercase;
&:before {
content: "";
display: block;
}
}
& > dl > dd {
display: inline;
margin-left: 1em;
}
}

View File

@@ -10,6 +10,7 @@ import { PushTransactionComponent } from '../components/push-transaction/push-tr
import { BlocksList } from '../components/blocks-list/blocks-list.component';
import { AssetGroupComponent } from '../components/assets/asset-group/asset-group.component';
import { AssetsComponent } from '../components/assets/assets.component';
import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component'
import { AssetComponent } from '../components/asset/asset.component';
import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component';
@@ -73,6 +74,11 @@ const routes: Routes = [
data: { networks: ['liquid'] },
component: AssetsComponent,
},
{
path: 'featured',
data: { networks: ['liquid'] },
component: AssetsFeaturedComponent,
},
{
path: 'asset/:id',
data: { networkSpecific: true },

View File

@@ -13,7 +13,7 @@ export class AudioService {
} catch (e) {}
}
public playSound(name: 'magic' | 'chime' | 'cha-ching' | 'bright-harmony') {
public playSound(name: 'magic' | 'chime' | 'cha-ching' | 'bright-harmony' | 'wind-chimes-harp-ascend' | 'ascend-chime-cartoon') {
if (this.isPlaying || !this.audio) {
return;
}

View File

@@ -183,14 +183,18 @@ export class WebsocketService {
}
startTrackMempoolBlock(block: number) {
this.websocketSubject.next({ 'track-mempool-block': block });
this.isTrackingMempoolBlock = true
this.trackingMempoolBlock = block
// skip duplicate tracking requests
if (this.trackingMempoolBlock !== block) {
this.websocketSubject.next({ 'track-mempool-block': block });
this.isTrackingMempoolBlock = true;
this.trackingMempoolBlock = block;
}
}
stopTrackMempoolBlock() {
this.websocketSubject.next({ 'track-mempool-block': -1 });
this.isTrackingMempoolBlock = false
this.isTrackingMempoolBlock = false;
this.trackingMempoolBlock = null;
}
startTrackRbf(mode: 'all' | 'fullRbf') {

View File

@@ -7,11 +7,11 @@
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126"></app-svg-images>
</div>
<p class="d-block d-sm-none">
<p class="explore-tagline-mobile">
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
<ng-template [ngIf]="locale.substr(0, 2) === 'en'"> &trade;</ng-template>
</p>
<div class="site-options float-right d-flex justify-content-center align-items-center" [class]="{'services': isServicesPage}">
<div class="site-options language-selector d-flex justify-content-center align-items-center" [class]="{'services': isServicesPage}">
<div class="selector">
<app-language-selector></app-language-selector>
</div>
@@ -26,11 +26,11 @@
<span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In</span>
</a>
</div>
<a *ngIf="servicesEnabled" class="btn btn-purple sponsor d-flex d-sm-none justify-content-center ml-auto mr-auto mt-3 mb-2" [routerLink]="['/login' | relativeUrl]">
<a *ngIf="servicesEnabled" class="btn btn-purple sponsor d-flex d-sm-none justify-content-center ml-auto mr-auto mt-0 mb-2" [routerLink]="['/login' | relativeUrl]">
<span *ngIf="loggedIn" i18n="shared.my-account">My Account</span>
<span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In</span>
</a>
<p class="d-none d-sm-block">
<p class="explore-tagline-desktop">
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
<ng-template [ngIf]="locale.substr(0, 2) === 'en'"> &trade;</ng-template>
</p>

View File

@@ -132,10 +132,36 @@ footer .row.version p a {
footer .sponsor {
height: 31px;
align-items: center;
margin-right: 5px;
margin-left: 5px;
max-width: 160px;
}
.explore-tagline-desktop {
display: none;
}
.explore-tagline-mobile {
display: block;
}
@media (min-width: 901px) {
:host-context(.ltr-layout) .language-selector {
float: right !important;
}
:host-context(.rtl-layout) .language-selector {
float: left !important;
}
.explore-tagline-desktop {
display: block;
}
.explore-tagline-mobile {
display: none;
}
}
@media (max-width: 1200px) {
.main-logo {
@@ -195,10 +221,6 @@ footer .sponsor {
float: none;
margin-top: 15px;
}
footer .selector:not(:last-child) {
margin-right: 10px;
}
}
@media (max-width: 1147px) {

View File

@@ -10,8 +10,9 @@ export class RelativeUrlPipe implements PipeTransform {
private stateService: StateService,
) { }
transform(value: string): string {
let network = this.stateService.network;
transform(value: string, swapNetwork?: string): string {
let network = swapNetwork || this.stateService.network;
if (network === 'mainnet') network = '';
if (this.stateService.env.BASE_MODULE === 'liquid' && network === 'liquidtestnet') {
network = 'testnet';
} else if (this.stateService.env.BASE_MODULE !== 'mempool') {

View File

@@ -0,0 +1,343 @@
import { Env } from '../services/state.service';
// all base58 characters
const BASE58_CHARS = `[a-km-zA-HJ-NP-Z1-9]`;
// all bech32 characters (after the separator)
const BECH32_CHARS_LW = `[ac-hj-np-z02-9]`;
const BECH32_CHARS_UP = `[AC-HJ-NP-Z02-9]`;
// Hex characters
const HEX_CHARS = `[a-fA-F0-9]`;
// A regex to say "A single 0 OR any number with no leading zeroes"
// Capped at 9 digits so as to not be confused with lightning channel IDs (which are around 17 digits)
// (?: // Start a non-capturing group
// 0 // A single 0
// | // OR
// [1-9][0-9]{0,8} // Any succession of numbers up to 9 digits starting with 1-9
// ) // End the non-capturing group.
const ZERO_INDEX_NUMBER_CHARS = `(?:0|[1-9][0-9]{0,8})`;
// Simple digits only regex
const NUMBER_CHARS = `[0-9]`;
// Formatting of the address regex is for readability,
// We should ignore formatting it with automated formatting tools like prettier.
//
// prettier-ignore
const ADDRESS_CHARS: {
[k in Network]: {
base58: string;
bech32: string;
};
} = {
mainnet: {
base58: `[13]` // Starts with a single 1 or 3
+ BASE58_CHARS
+ `{26,33}`, // Repeat the previous char 26-33 times.
// Version byte 0x00 (P2PKH) can be as short as 27 characters, up to 34 length
// P2SH must be 34 length
bech32: `(?:`
+ `bc1` // Starts with bc1
+ BECH32_CHARS_LW
+ `{20,100}` // As per bech32, 6 char checksum is minimum
+ `|`
+ `BC1` // All upper case version
+ BECH32_CHARS_UP
+ `{20,100}`
+ `)`,
},
testnet: {
base58: `[mn2]` // Starts with a single m, n, or 2 (P2PKH is m or n, 2 is P2SH)
+ BASE58_CHARS
+ `{33,34}`, // m|n is 34 length, 2 is 35 length (We match the first letter separately)
bech32: `(?:`
+ `tb1` // Starts with tb1
+ BECH32_CHARS_LW
+ `{20,100}` // As per bech32, 6 char checksum is minimum
+ `|`
+ `TB1` // All upper case version
+ BECH32_CHARS_UP
+ `{20,100}`
+ `)`,
},
signet: {
base58: `[mn2]`
+ BASE58_CHARS
+ `{33,34}`,
bech32: `(?:`
+ `tb1` // Starts with tb1
+ BECH32_CHARS_LW
+ `{20,100}`
+ `|`
+ `TB1` // All upper case version
+ BECH32_CHARS_UP
+ `{20,100}`
+ `)`,
},
liquid: {
base58: `[GHPQ]` // G|H is P2PKH, P|Q is P2SH
+ BASE58_CHARS
+ `{33}`, // All min-max lengths are 34
bech32: `(?:`
+ `(?:` // bech32 liquid starts with ex1 or lq1
+ `ex1`
+ `|`
+ `lq1`
+ `)`
+ BECH32_CHARS_LW // blech32 and bech32 are the same alphabet and protocol, different checksums.
+ `{20,100}`
+ `|`
+ `(?:` // Same as above but all upper case
+ `EX1`
+ `|`
+ `LQ1`
+ `)`
+ BECH32_CHARS_UP
+ `{20,100}`
+ `)`,
},
liquidtestnet: {
base58: `[89]` // ???(TODO: find version) is P2PKH, 8|9 is P2SH
+ BASE58_CHARS
+ `{33}`, // P2PKH is ???(TODO: find size), P2SH is 34
bech32: `(?:`
+ `(?:` // bech32 liquid testnet starts with tex or tlq
+ `tex1` // TODO: Why does mempool use this and not ert|el like in the elements source?
+ `|`
+ `tlq1` // TODO: does this exist?
+ `)`
+ BECH32_CHARS_LW // blech32 and bech32 are the same alphabet and protocol, different checksums.
+ `{20,100}`
+ `|`
+ `(?:` // Same as above but all upper case
+ `TEX1`
+ `|`
+ `TLQ1`
+ `)`
+ BECH32_CHARS_UP
+ `{20,100}`
+ `)`,
},
bisq: {
base58: `(?:[bB][13]` // b or B at the start, followed by a single 1 or 3
+ BASE58_CHARS
+ `{26,33})`,
bech32: `(?:`
+ `[bB]bc1` // b or B at the start, followed by bc1
+ BECH32_CHARS_LW
+ `{20,100}`
+ `|`
+ `[bB]BC1` // b or B at the start, followed by BC1
+ BECH32_CHARS_UP
+ `{20,100}`
+ `)`,
},
}
type RegexTypeNoAddrNoBlockHash = | `transaction` | `blockheight` | `date` | `timestamp`;
export type RegexType = `address` | `blockhash` | RegexTypeNoAddrNoBlockHash;
export const NETWORKS = [`testnet`, `signet`, `liquid`, `liquidtestnet`, `bisq`, `mainnet`] as const;
export type Network = typeof NETWORKS[number]; // Turn const array into union type
export const ADDRESS_REGEXES: [RegExp, Network][] = NETWORKS
.map(network => [getRegex('address', network), network])
export function findOtherNetworks(address: string, skipNetwork: Network, env: Env): { network: Network, address: string, isNetworkAvailable: boolean }[] {
return ADDRESS_REGEXES
.filter(([regex, network]) => network !== skipNetwork && regex.test(address))
.map(([, network]) => ({ network, address, isNetworkAvailable: isNetworkAvailable(network, env) }));
}
function isNetworkAvailable(network: Network, env: Env): boolean {
switch (network) {
case 'testnet':
return env.TESTNET_ENABLED === true;
case 'signet':
return env.SIGNET_ENABLED === true;
case 'liquid':
return env.LIQUID_ENABLED === true;
case 'liquidtestnet':
return env.LIQUID_TESTNET_ENABLED === true;
case 'bisq':
return env.BISQ_ENABLED === true;
case 'mainnet':
return true; // There is no "MAINNET_ENABLED" flag
default:
return false;
}
}
export function needBaseModuleChange(fromBaseModule: 'mempool' | 'liquid' | 'bisq', toNetwork: Network): boolean {
if (!toNetwork) return false; // No target network means no change needed
if (fromBaseModule === 'mempool') {
return toNetwork !== 'mainnet' && toNetwork !== 'testnet' && toNetwork !== 'signet';
}
if (fromBaseModule === 'liquid') {
return toNetwork !== 'liquid' && toNetwork !== 'liquidtestnet';
}
if (fromBaseModule === 'bisq') {
return toNetwork !== 'bisq';
}
}
export function getTargetUrl(toNetwork: Network, address: string, env: Env): string {
let targetUrl = '';
if (toNetwork === 'liquid' || toNetwork === 'liquidtestnet') {
targetUrl = env.LIQUID_WEBSITE_URL;
targetUrl += (toNetwork === 'liquidtestnet' ? '/testnet' : '');
targetUrl += '/address/';
targetUrl += address;
}
if (toNetwork === 'bisq') {
targetUrl = env.BISQ_WEBSITE_URL;
targetUrl += '/address/';
targetUrl += address;
}
if (toNetwork === 'mainnet' || toNetwork === 'testnet' || toNetwork === 'signet') {
targetUrl = env.MEMPOOL_WEBSITE_URL;
targetUrl += (toNetwork === 'mainnet' ? '' : `/${toNetwork}`);
targetUrl += '/address/';
targetUrl += address;
}
return targetUrl;
}
export function getRegex(type: RegexTypeNoAddrNoBlockHash): RegExp;
export function getRegex(type: 'address', network: Network): RegExp;
export function getRegex(type: 'blockhash', network: Network): RegExp;
export function getRegex(type: RegexType, network?: Network): RegExp {
let regex = `^`; // ^ = Start of string
switch (type) {
// Match a block height number
// [Testing Order]: any order is fine
case `blockheight`:
regex += ZERO_INDEX_NUMBER_CHARS; // block height is a 0 indexed number
break;
// Match a 32 byte block hash in hex.
// [Testing Order]: Must always be tested before `transaction`
case `blockhash`:
if (!network) {
throw new Error(`Must pass network when type is blockhash`);
}
let leadingZeroes: number;
switch (network) {
case `mainnet`:
leadingZeroes = 8; // Assumes at least 32 bits of difficulty
break;
case `testnet`:
leadingZeroes = 8; // Assumes at least 32 bits of difficulty
break;
case `signet`:
leadingZeroes = 5;
break;
case `liquid`:
leadingZeroes = 8; // We are not interested in Liquid block hashes
break;
case `liquidtestnet`:
leadingZeroes = 8; // We are not interested in Liquid block hashes
break;
case `bisq`:
leadingZeroes = 8; // Assumes at least 32 bits of difficulty
break;
default:
throw new Error(`Invalid Network ${network} (Unreachable error in TypeScript)`);
}
regex += `0{${leadingZeroes}}`;
regex += `${HEX_CHARS}{${64 - leadingZeroes}}`; // Exactly 64 hex letters/numbers
break;
// Match a 32 byte tx hash in hex. Contains optional output index specifier.
// [Testing Order]: Must always be tested after `blockhash`
case `transaction`:
regex += `${HEX_CHARS}{64}`; // Exactly 64 hex letters/numbers
regex += `(?:`; // Start a non-capturing group
regex += `:`; // 1 instances of the symbol ":"
regex += ZERO_INDEX_NUMBER_CHARS; // A zero indexed number
regex += `)?`; // End the non-capturing group. This group appears 0 or 1 times
break;
// Match any one of the many address types
// [Testing Order]: While possible that a bech32 address happens to be 64 hex
// characters in the future (current lengths are not 64), it is highly unlikely
// Order therefore, does not matter.
case `address`:
if (!network) {
throw new Error(`Must pass network when type is address`);
}
regex += `(?:`; // Start a non-capturing group (each network has multiple options)
switch (network) {
case `mainnet`:
regex += ADDRESS_CHARS.mainnet.base58;
regex += `|`; // OR
regex += ADDRESS_CHARS.mainnet.bech32;
regex += `|`; // OR
regex += `04${HEX_CHARS}{128}`; // Uncompressed pubkey
regex += `|`; // OR
regex += `(?:02|03)${HEX_CHARS}{64}`; // Compressed pubkey
break;
case `testnet`:
regex += ADDRESS_CHARS.testnet.base58;
regex += `|`; // OR
regex += ADDRESS_CHARS.testnet.bech32;
regex += `|`; // OR
regex += `04${HEX_CHARS}{128}`; // Uncompressed pubkey
regex += `|`; // OR
regex += `(?:02|03)${HEX_CHARS}{64}`; // Compressed pubkey
break;
case `signet`:
regex += ADDRESS_CHARS.signet.base58;
regex += `|`; // OR
regex += ADDRESS_CHARS.signet.bech32;
regex += `|`; // OR
regex += `04${HEX_CHARS}{128}`; // Uncompressed pubkey
regex += `|`; // OR
regex += `(?:02|03)${HEX_CHARS}{64}`; // Compressed pubkey
break;
case `liquid`:
regex += ADDRESS_CHARS.liquid.base58;
regex += `|`; // OR
regex += ADDRESS_CHARS.liquid.bech32;
break;
case `liquidtestnet`:
regex += ADDRESS_CHARS.liquidtestnet.base58;
regex += `|`; // OR
regex += ADDRESS_CHARS.liquidtestnet.bech32;
break;
case `bisq`:
regex += ADDRESS_CHARS.bisq.base58;
regex += `|`; // OR
regex += ADDRESS_CHARS.bisq.bech32;
break;
default:
throw new Error(`Invalid Network ${network} (Unreachable error in TypeScript)`);
}
regex += `)`; // End the non-capturing group
break;
// Match a date in the format YYYY-MM-DD (optional: HH:MM)
// [Testing Order]: any order is fine
case `date`:
regex += `(?:`; // Start a non-capturing group
regex += `${NUMBER_CHARS}{4}`; // Exactly 4 digits
regex += `[-/]`; // 1 instance of the symbol "-" or "/"
regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits
regex += `[-/]`; // 1 instance of the symbol "-" or "/"
regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits
regex += `(?:`; // Start a non-capturing group
regex += ` `; // 1 instance of the symbol " "
regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits
regex += `:`; // 1 instance of the symbol ":"
regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits
regex += `)?`; // End the non-capturing group. This group appears 0 or 1 times
regex += `)`; // End the non-capturing group
break;
// Match a unix timestamp
// [Testing Order]: any order is fine
case `timestamp`:
regex += `${NUMBER_CHARS}{10}`; // Exactly 10 digits
break;
default:
throw new Error(`Invalid RegexType ${type} (Unreachable error in TypeScript)`);
}
regex += `$`; // $ = End of string
return new RegExp(regex);
}

View File

@@ -322,7 +322,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
ClockFaceComponent,
OnlyVsizeDirective,
OnlyWeightDirective
OnlyWeightDirective,
]
})
export class SharedModule {

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

View File

@@ -1191,3 +1191,7 @@ app-global-footer {
line-height: 0.5;
border-radius: 0.2rem;
}
.info-link fa-icon {
color: rgba(255, 255, 255, 0.4);
}