Compare commits
74 Commits
mononaut/r
...
v3.0.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
975588a862 | ||
|
|
093649136e | ||
|
|
018d4230e2 | ||
|
|
6c66da5426 | ||
|
|
5b9b0d0968 | ||
|
|
ddf1bc0654 | ||
|
|
419b40884a | ||
|
|
1f05e7b366 | ||
|
|
a9d94bdafd | ||
|
|
18b1805ff3 | ||
|
|
4044ca3877 | ||
|
|
66386f2bcc | ||
|
|
d0bf26bcef | ||
|
|
e918e1fdab | ||
|
|
968a26d2cd | ||
|
|
43fde86e9d | ||
|
|
587c7f6d59 | ||
|
|
f431ca742f | ||
|
|
7e16c5e8c4 | ||
|
|
8701ba8546 | ||
|
|
42912ff007 | ||
|
|
6d8a72301c | ||
|
|
1cdbde680c | ||
|
|
ec21b3d06f | ||
|
|
e8e43c074b | ||
|
|
dc50b40b69 | ||
|
|
e41d6bc2e6 | ||
|
|
cb7938baa1 | ||
|
|
4d6b62c7de | ||
|
|
7bf053afe5 | ||
|
|
d8900d40c7 | ||
|
|
841f18f481 | ||
|
|
b0d34d4e48 | ||
|
|
c684834c42 | ||
|
|
46e989ab27 | ||
|
|
2c19e44fbf | ||
|
|
dab72f669d | ||
|
|
16537e8018 | ||
|
|
a3b713cc74 | ||
|
|
17697e669a | ||
|
|
035068a72e | ||
|
|
086ae6978a | ||
|
|
951b946be9 | ||
|
|
5fc2205b54 | ||
|
|
18e412939d | ||
|
|
69dc21d232 | ||
|
|
521d2fc650 | ||
|
|
ac250ebe6c | ||
|
|
596aada169 | ||
|
|
c4df05b581 | ||
|
|
7fb699a02b | ||
|
|
e8fc1e8522 | ||
|
|
5f14b32a06 | ||
|
|
e2d7c82553 | ||
|
|
7259a00c8d | ||
|
|
7c6c1187bb | ||
|
|
9f4c351e3d | ||
|
|
329c1330f4 | ||
|
|
d55d5db01d | ||
|
|
55cd2f5678 | ||
|
|
69747a0028 | ||
|
|
45c1e05ebc | ||
|
|
8713d33d1a | ||
|
|
99aa6c9ed3 | ||
|
|
000524691a | ||
|
|
290de43df0 | ||
|
|
33ac4056d8 | ||
|
|
4ef1df47a3 | ||
|
|
346c024ddf | ||
|
|
42bd08c744 | ||
|
|
ba8ea7d0d1 | ||
|
|
445d600633 | ||
|
|
ab8386adea | ||
|
|
b7474b29e4 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||
strategy:
|
||||
matrix:
|
||||
node: ["18", "20"]
|
||||
node: ["20", "21"]
|
||||
flavor: ["dev", "prod"]
|
||||
fail-fast: false
|
||||
runs-on: "ubuntu-latest"
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||
strategy:
|
||||
matrix:
|
||||
node: ["18", "20"]
|
||||
node: ["20", "21"]
|
||||
flavor: ["dev", "prod"]
|
||||
fail-fast: false
|
||||
runs-on: "ubuntu-latest"
|
||||
|
||||
335
backend/package-lock.json
generated
335
backend/package-lock.json
generated
@@ -9,7 +9,6 @@
|
||||
"version": "3.0.0-dev",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.23.2",
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^18.15.3",
|
||||
"axios": "~1.6.1",
|
||||
@@ -26,7 +25,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/core": "^7.23.2",
|
||||
"@babel/core": "^7.24.0",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/express": "^4.17.17",
|
||||
@@ -65,12 +64,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.22.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
|
||||
"integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
|
||||
"version": "7.23.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
|
||||
"integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/highlight": "^7.22.13",
|
||||
"@babel/highlight": "^7.23.4",
|
||||
"chalk": "^2.4.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -78,30 +77,30 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz",
|
||||
"integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==",
|
||||
"version": "7.23.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz",
|
||||
"integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz",
|
||||
"integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==",
|
||||
"version": "7.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
|
||||
"integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.22.13",
|
||||
"@babel/generator": "^7.23.0",
|
||||
"@babel/helper-compilation-targets": "^7.22.15",
|
||||
"@babel/helper-module-transforms": "^7.23.0",
|
||||
"@babel/helpers": "^7.23.2",
|
||||
"@babel/parser": "^7.23.0",
|
||||
"@babel/template": "^7.22.15",
|
||||
"@babel/traverse": "^7.23.2",
|
||||
"@babel/types": "^7.23.0",
|
||||
"@babel/code-frame": "^7.23.5",
|
||||
"@babel/generator": "^7.23.6",
|
||||
"@babel/helper-compilation-targets": "^7.23.6",
|
||||
"@babel/helper-module-transforms": "^7.23.3",
|
||||
"@babel/helpers": "^7.24.0",
|
||||
"@babel/parser": "^7.24.0",
|
||||
"@babel/template": "^7.24.0",
|
||||
"@babel/traverse": "^7.24.0",
|
||||
"@babel/types": "^7.24.0",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
@@ -123,12 +122,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
|
||||
"integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
|
||||
"version": "7.23.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
|
||||
"integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.23.0",
|
||||
"@babel/types": "^7.23.6",
|
||||
"@jridgewell/gen-mapping": "^0.3.2",
|
||||
"@jridgewell/trace-mapping": "^0.3.17",
|
||||
"jsesc": "^2.5.1"
|
||||
@@ -152,14 +151,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.22.15",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz",
|
||||
"integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==",
|
||||
"version": "7.23.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz",
|
||||
"integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.22.9",
|
||||
"@babel/helper-validator-option": "^7.22.15",
|
||||
"browserslist": "^4.21.9",
|
||||
"@babel/compat-data": "^7.23.5",
|
||||
"@babel/helper-validator-option": "^7.23.5",
|
||||
"browserslist": "^4.22.2",
|
||||
"lru-cache": "^5.1.1",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
@@ -214,9 +213,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz",
|
||||
"integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==",
|
||||
"version": "7.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz",
|
||||
"integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-environment-visitor": "^7.22.20",
|
||||
@@ -266,9 +265,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
|
||||
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
|
||||
"version": "7.23.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
|
||||
"integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -284,32 +283,32 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-option": {
|
||||
"version": "7.22.15",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz",
|
||||
"integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==",
|
||||
"version": "7.23.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz",
|
||||
"integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz",
|
||||
"integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==",
|
||||
"version": "7.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz",
|
||||
"integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.22.15",
|
||||
"@babel/traverse": "^7.23.2",
|
||||
"@babel/types": "^7.23.0"
|
||||
"@babel/template": "^7.24.0",
|
||||
"@babel/traverse": "^7.24.0",
|
||||
"@babel/types": "^7.24.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight": {
|
||||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
|
||||
"integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
|
||||
"version": "7.23.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
|
||||
"integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
@@ -321,9 +320,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
|
||||
"integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
|
||||
"version": "7.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz",
|
||||
"integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -510,34 +509,34 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.22.15",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
|
||||
"integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
|
||||
"version": "7.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz",
|
||||
"integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.22.13",
|
||||
"@babel/parser": "^7.22.15",
|
||||
"@babel/types": "^7.22.15"
|
||||
"@babel/code-frame": "^7.23.5",
|
||||
"@babel/parser": "^7.24.0",
|
||||
"@babel/types": "^7.24.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
|
||||
"integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
|
||||
"version": "7.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz",
|
||||
"integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.22.13",
|
||||
"@babel/generator": "^7.23.0",
|
||||
"@babel/code-frame": "^7.23.5",
|
||||
"@babel/generator": "^7.23.6",
|
||||
"@babel/helper-environment-visitor": "^7.22.20",
|
||||
"@babel/helper-function-name": "^7.23.0",
|
||||
"@babel/helper-hoist-variables": "^7.22.5",
|
||||
"@babel/helper-split-export-declaration": "^7.22.6",
|
||||
"@babel/parser": "^7.23.0",
|
||||
"@babel/types": "^7.23.0",
|
||||
"debug": "^4.1.0",
|
||||
"@babel/parser": "^7.24.0",
|
||||
"@babel/types": "^7.24.0",
|
||||
"debug": "^4.3.1",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -545,12 +544,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
|
||||
"integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
|
||||
"version": "7.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz",
|
||||
"integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.22.5",
|
||||
"@babel/helper-string-parser": "^7.23.4",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
@@ -2594,9 +2593,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
|
||||
"integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
|
||||
"version": "4.23.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
|
||||
"integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2613,9 +2612,9 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001541",
|
||||
"electron-to-chromium": "^1.4.535",
|
||||
"node-releases": "^2.0.13",
|
||||
"caniuse-lite": "^1.0.30001587",
|
||||
"electron-to-chromium": "^1.4.668",
|
||||
"node-releases": "^2.0.14",
|
||||
"update-browserslist-db": "^1.0.13"
|
||||
},
|
||||
"bin": {
|
||||
@@ -2708,9 +2707,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001547",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001547.tgz",
|
||||
"integrity": "sha512-W7CrtIModMAxobGhz8iXmDfuJiiKg1WADMO/9x7/CLNin5cpSbuBjooyoIUVB5eyCc36QuTVlkVa1iB2S5+/eA==",
|
||||
"version": "1.0.30001591",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz",
|
||||
"integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3031,9 +3030,9 @@
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.4.551",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.551.tgz",
|
||||
"integrity": "sha512-/Ng/W/kFv7wdEHYzxdK7Cv0BHEGSkSB3M0Ssl8Ndr1eMiYeas/+Mv4cNaDqamqWx6nd2uQZfPz6g25z25M/sdw==",
|
||||
"version": "1.4.686",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.686.tgz",
|
||||
"integrity": "sha512-3avY1B+vUzNxEgkBDpKOP8WarvUAEwpRaiCL0He5OKWEFxzaOFiq4WoZEZe7qh0ReS7DiWoHMnYoQCKxNZNzSg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/emittery": {
|
||||
@@ -6192,9 +6191,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.13",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
|
||||
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
|
||||
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
@@ -7695,37 +7694,37 @@
|
||||
}
|
||||
},
|
||||
"@babel/code-frame": {
|
||||
"version": "7.22.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
|
||||
"integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
|
||||
"version": "7.23.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
|
||||
"integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/highlight": "^7.22.13",
|
||||
"@babel/highlight": "^7.23.4",
|
||||
"chalk": "^2.4.2"
|
||||
}
|
||||
},
|
||||
"@babel/compat-data": {
|
||||
"version": "7.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz",
|
||||
"integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==",
|
||||
"version": "7.23.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz",
|
||||
"integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/core": {
|
||||
"version": "7.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz",
|
||||
"integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==",
|
||||
"version": "7.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
|
||||
"integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.22.13",
|
||||
"@babel/generator": "^7.23.0",
|
||||
"@babel/helper-compilation-targets": "^7.22.15",
|
||||
"@babel/helper-module-transforms": "^7.23.0",
|
||||
"@babel/helpers": "^7.23.2",
|
||||
"@babel/parser": "^7.23.0",
|
||||
"@babel/template": "^7.22.15",
|
||||
"@babel/traverse": "^7.23.2",
|
||||
"@babel/types": "^7.23.0",
|
||||
"@babel/code-frame": "^7.23.5",
|
||||
"@babel/generator": "^7.23.6",
|
||||
"@babel/helper-compilation-targets": "^7.23.6",
|
||||
"@babel/helper-module-transforms": "^7.23.3",
|
||||
"@babel/helpers": "^7.24.0",
|
||||
"@babel/parser": "^7.24.0",
|
||||
"@babel/template": "^7.24.0",
|
||||
"@babel/traverse": "^7.24.0",
|
||||
"@babel/types": "^7.24.0",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
@@ -7742,12 +7741,12 @@
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
|
||||
"integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
|
||||
"version": "7.23.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
|
||||
"integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.23.0",
|
||||
"@babel/types": "^7.23.6",
|
||||
"@jridgewell/gen-mapping": "^0.3.2",
|
||||
"@jridgewell/trace-mapping": "^0.3.17",
|
||||
"jsesc": "^2.5.1"
|
||||
@@ -7767,14 +7766,14 @@
|
||||
}
|
||||
},
|
||||
"@babel/helper-compilation-targets": {
|
||||
"version": "7.22.15",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz",
|
||||
"integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==",
|
||||
"version": "7.23.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz",
|
||||
"integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/compat-data": "^7.22.9",
|
||||
"@babel/helper-validator-option": "^7.22.15",
|
||||
"browserslist": "^4.21.9",
|
||||
"@babel/compat-data": "^7.23.5",
|
||||
"@babel/helper-validator-option": "^7.23.5",
|
||||
"browserslist": "^4.22.2",
|
||||
"lru-cache": "^5.1.1",
|
||||
"semver": "^6.3.1"
|
||||
}
|
||||
@@ -7814,9 +7813,9 @@
|
||||
}
|
||||
},
|
||||
"@babel/helper-module-transforms": {
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz",
|
||||
"integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==",
|
||||
"version": "7.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz",
|
||||
"integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-environment-visitor": "^7.22.20",
|
||||
@@ -7851,9 +7850,9 @@
|
||||
}
|
||||
},
|
||||
"@babel/helper-string-parser": {
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
|
||||
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
|
||||
"version": "7.23.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
|
||||
"integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
@@ -7863,26 +7862,26 @@
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helper-validator-option": {
|
||||
"version": "7.22.15",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz",
|
||||
"integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==",
|
||||
"version": "7.23.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz",
|
||||
"integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helpers": {
|
||||
"version": "7.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz",
|
||||
"integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==",
|
||||
"version": "7.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz",
|
||||
"integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/template": "^7.22.15",
|
||||
"@babel/traverse": "^7.23.2",
|
||||
"@babel/types": "^7.23.0"
|
||||
"@babel/template": "^7.24.0",
|
||||
"@babel/traverse": "^7.24.0",
|
||||
"@babel/types": "^7.24.0"
|
||||
}
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
|
||||
"integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
|
||||
"version": "7.23.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
|
||||
"integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
@@ -7891,9 +7890,9 @@
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
|
||||
"integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
|
||||
"version": "7.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz",
|
||||
"integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/plugin-syntax-async-generators": {
|
||||
@@ -8023,41 +8022,41 @@
|
||||
}
|
||||
},
|
||||
"@babel/template": {
|
||||
"version": "7.22.15",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
|
||||
"integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
|
||||
"version": "7.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz",
|
||||
"integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.22.13",
|
||||
"@babel/parser": "^7.22.15",
|
||||
"@babel/types": "^7.22.15"
|
||||
"@babel/code-frame": "^7.23.5",
|
||||
"@babel/parser": "^7.24.0",
|
||||
"@babel/types": "^7.24.0"
|
||||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
|
||||
"integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
|
||||
"version": "7.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz",
|
||||
"integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.22.13",
|
||||
"@babel/generator": "^7.23.0",
|
||||
"@babel/code-frame": "^7.23.5",
|
||||
"@babel/generator": "^7.23.6",
|
||||
"@babel/helper-environment-visitor": "^7.22.20",
|
||||
"@babel/helper-function-name": "^7.23.0",
|
||||
"@babel/helper-hoist-variables": "^7.22.5",
|
||||
"@babel/helper-split-export-declaration": "^7.22.6",
|
||||
"@babel/parser": "^7.23.0",
|
||||
"@babel/types": "^7.23.0",
|
||||
"debug": "^4.1.0",
|
||||
"@babel/parser": "^7.24.0",
|
||||
"@babel/types": "^7.24.0",
|
||||
"debug": "^4.3.1",
|
||||
"globals": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
|
||||
"integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
|
||||
"version": "7.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz",
|
||||
"integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-string-parser": "^7.22.5",
|
||||
"@babel/helper-string-parser": "^7.23.4",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
@@ -9633,14 +9632,14 @@
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
|
||||
"integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
|
||||
"version": "4.23.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
|
||||
"integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"caniuse-lite": "^1.0.30001541",
|
||||
"electron-to-chromium": "^1.4.535",
|
||||
"node-releases": "^2.0.13",
|
||||
"caniuse-lite": "^1.0.30001587",
|
||||
"electron-to-chromium": "^1.4.668",
|
||||
"node-releases": "^2.0.14",
|
||||
"update-browserslist-db": "^1.0.13"
|
||||
}
|
||||
},
|
||||
@@ -9712,9 +9711,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001547",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001547.tgz",
|
||||
"integrity": "sha512-W7CrtIModMAxobGhz8iXmDfuJiiKg1WADMO/9x7/CLNin5cpSbuBjooyoIUVB5eyCc36QuTVlkVa1iB2S5+/eA==",
|
||||
"version": "1.0.30001591",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz",
|
||||
"integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==",
|
||||
"dev": true
|
||||
},
|
||||
"chalk": {
|
||||
@@ -9942,9 +9941,9 @@
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.4.551",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.551.tgz",
|
||||
"integrity": "sha512-/Ng/W/kFv7wdEHYzxdK7Cv0BHEGSkSB3M0Ssl8Ndr1eMiYeas/+Mv4cNaDqamqWx6nd2uQZfPz6g25z25M/sdw==",
|
||||
"version": "1.4.686",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.686.tgz",
|
||||
"integrity": "sha512-3avY1B+vUzNxEgkBDpKOP8WarvUAEwpRaiCL0He5OKWEFxzaOFiq4WoZEZe7qh0ReS7DiWoHMnYoQCKxNZNzSg==",
|
||||
"dev": true
|
||||
},
|
||||
"emittery": {
|
||||
@@ -12298,9 +12297,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node-releases": {
|
||||
"version": "2.0.13",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
|
||||
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
|
||||
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
|
||||
"dev": true
|
||||
},
|
||||
"normalize-path": {
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"rust-build": "npm run rust-clean && cd rust-gbt && npm run build-release"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.23.2",
|
||||
"@babel/core": "^7.24.0",
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^18.15.3",
|
||||
"axios": "~1.6.1",
|
||||
@@ -56,7 +56,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/core": "^7.23.2",
|
||||
"@babel/core": "^7.24.0",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/express": "^4.17.17",
|
||||
|
||||
@@ -137,10 +137,6 @@
|
||||
"AUDIT_START_HEIGHT": 774000,
|
||||
"SERVERS": []
|
||||
},
|
||||
"REBROADCAST": {
|
||||
"ENABLED": false,
|
||||
"FREQUENCY": 3600
|
||||
},
|
||||
"MEMPOOL_SERVICES": {
|
||||
"API": "",
|
||||
"ACCELERATIONS": false
|
||||
|
||||
@@ -11,9 +11,35 @@ describe('Mempool Difficulty Adjustment', () => {
|
||||
};
|
||||
|
||||
const vectors = [
|
||||
[ // Vector 1
|
||||
[ // Vector 1 (normal adjustment)
|
||||
[ // Inputs
|
||||
dt('2024-02-02T15:42:06.000Z'), // Last DA time (in seconds)
|
||||
dt('2024-02-08T14:43:05.000Z'), // timestamp of 504 blocks ago (in seconds)
|
||||
dt('2024-02-11T22:43:01.000Z'), // Current time (now) (in seconds)
|
||||
830027, // Current block height
|
||||
7.333505241141637, // Previous retarget % (Passed through)
|
||||
'mainnet', // Network (if testnet, next value is non-zero)
|
||||
0, // Latest block timestamp in seconds (only used if difficulty already locked in)
|
||||
],
|
||||
{ // Expected Result
|
||||
progressPercent: 71.97420634920636,
|
||||
difficultyChange: 8.512745140778843,
|
||||
estimatedRetargetDate: 1708004001715,
|
||||
remainingBlocks: 565,
|
||||
remainingTime: 312620715,
|
||||
previousRetarget: 7.333505241141637,
|
||||
previousTime: 1706888526,
|
||||
nextRetargetHeight: 830592,
|
||||
timeAvg: 553311,
|
||||
adjustedTimeAvg: 553311,
|
||||
timeOffset: 0,
|
||||
expectedBlocks: 1338.0916666666667,
|
||||
},
|
||||
],
|
||||
[ // Vector 2 (within quarter-epoch overlap)
|
||||
[ // Inputs
|
||||
dt('2022-08-18T11:07:00.000Z'), // Last DA time (in seconds)
|
||||
dt('2022-08-16T03:16:54.000Z'), // timestamp of 504 blocks ago (in seconds)
|
||||
dt('2022-08-19T14:03:53.000Z'), // Current time (now) (in seconds)
|
||||
750134, // Current block height
|
||||
0.6280047707459726, // Previous retarget % (Passed through)
|
||||
@@ -22,21 +48,23 @@ describe('Mempool Difficulty Adjustment', () => {
|
||||
],
|
||||
{ // Expected Result
|
||||
progressPercent: 9.027777777777777,
|
||||
difficultyChange: 13.180707740199772,
|
||||
estimatedRetargetDate: 1661895424692,
|
||||
difficultyChange: 1.0420538959004633,
|
||||
estimatedRetargetDate: 1662009048328,
|
||||
remainingBlocks: 1834,
|
||||
remainingTime: 977591692,
|
||||
remainingTime: 1091215328,
|
||||
previousRetarget: 0.6280047707459726,
|
||||
previousTime: 1660820820,
|
||||
nextRetargetHeight: 751968,
|
||||
timeAvg: 533038,
|
||||
adjustedTimeAvg: 594992,
|
||||
timeOffset: 0,
|
||||
expectedBlocks: 161.68833333333333,
|
||||
},
|
||||
],
|
||||
[ // Vector 2 (testnet)
|
||||
[ // Vector 3 (testnet)
|
||||
[ // Inputs
|
||||
dt('2022-08-18T11:07:00.000Z'), // Last DA time (in seconds)
|
||||
dt('2022-08-16T03:16:54.000Z'), // timestamp of 504 blocks ago (in seconds)
|
||||
dt('2022-08-19T14:03:53.000Z'), // Current time (now) (in seconds)
|
||||
750134, // Current block height
|
||||
0.6280047707459726, // Previous retarget % (Passed through)
|
||||
@@ -45,22 +73,24 @@ describe('Mempool Difficulty Adjustment', () => {
|
||||
],
|
||||
{ // Expected Result is same other than timeOffset
|
||||
progressPercent: 9.027777777777777,
|
||||
difficultyChange: 13.180707740199772,
|
||||
estimatedRetargetDate: 1661895424692,
|
||||
difficultyChange: 1.0420538959004633,
|
||||
estimatedRetargetDate: 1662009048328,
|
||||
remainingBlocks: 1834,
|
||||
remainingTime: 977591692,
|
||||
remainingTime: 1091215328,
|
||||
previousTime: 1660820820,
|
||||
previousRetarget: 0.6280047707459726,
|
||||
nextRetargetHeight: 751968,
|
||||
timeAvg: 533038,
|
||||
adjustedTimeAvg: 594992,
|
||||
timeOffset: -667000, // 11 min 7 seconds since last block (testnet only)
|
||||
// If we add time avg to abs(timeOffset) it makes exactly 1200000 ms, or 20 minutes
|
||||
expectedBlocks: 161.68833333333333,
|
||||
},
|
||||
],
|
||||
[ // Vector 3 (mainnet lock-in (epoch ending 788255))
|
||||
[ // Vector 4 (mainnet lock-in (epoch ending 788255))
|
||||
[ // Inputs
|
||||
dt('2023-04-20T09:57:33.000Z'), // Last DA time (in seconds)
|
||||
dt('2022-08-16T03:16:54.000Z'), // timestamp of 504 blocks ago (in seconds)
|
||||
dt('2023-05-04T14:54:09.000Z'), // Current time (now) (in seconds)
|
||||
788255, // Current block height
|
||||
1.7220298879531821, // Previous retarget % (Passed through)
|
||||
@@ -77,16 +107,17 @@ describe('Mempool Difficulty Adjustment', () => {
|
||||
previousTime: 1681984653,
|
||||
nextRetargetHeight: 788256,
|
||||
timeAvg: 609129,
|
||||
adjustedTimeAvg: 609129,
|
||||
timeOffset: 0,
|
||||
expectedBlocks: 2045.66,
|
||||
},
|
||||
],
|
||||
] as [[number, number, number, number, string, number], DifficultyAdjustment][];
|
||||
] as [[number, number, number, number, number, string, number], DifficultyAdjustment][];
|
||||
|
||||
for (const vector of vectors) {
|
||||
const result = calcDifficultyAdjustment(...vector[0]);
|
||||
// previousRetarget is passed through untouched
|
||||
expect(result.previousRetarget).toStrictEqual(vector[0][3]);
|
||||
expect(result.previousRetarget).toStrictEqual(vector[0][4]);
|
||||
expect(result).toStrictEqual(vector[1]);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -140,11 +140,6 @@ describe('Mempool Backend Config', () => {
|
||||
SERVERS: []
|
||||
});
|
||||
|
||||
expect(config.REBROADCAST).toStrictEqual({
|
||||
ENABLED: false,
|
||||
FREQUENCY: 3600
|
||||
});
|
||||
|
||||
expect(config.MEMPOOL_SERVICES).toStrictEqual({
|
||||
API: "",
|
||||
ACCELERATIONS: false,
|
||||
|
||||
@@ -29,7 +29,6 @@ import websocketHandler from './websocket-handler';
|
||||
import redisCache from './redis-cache';
|
||||
import rbfCache from './rbf-cache';
|
||||
import { calcBitsDifference } from './difficulty-adjustment';
|
||||
import rebroadcaster from './rebroadcaster';
|
||||
|
||||
class Blocks {
|
||||
private blocks: BlockExtended[] = [];
|
||||
@@ -38,8 +37,10 @@ class Blocks {
|
||||
private currentBits = 0;
|
||||
private lastDifficultyAdjustmentTime = 0;
|
||||
private previousDifficultyRetarget = 0;
|
||||
private quarterEpochBlockTime: number | null = null;
|
||||
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
|
||||
private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]) => Promise<void>)[] = [];
|
||||
private classifyingBlocks: boolean = false;
|
||||
|
||||
private mainLoopTimeout: number = 120000;
|
||||
|
||||
@@ -568,6 +569,11 @@ class Blocks {
|
||||
* [INDEXING] Index transaction classification flags for Goggles
|
||||
*/
|
||||
public async $classifyBlocks(): Promise<void> {
|
||||
if (this.classifyingBlocks) {
|
||||
return;
|
||||
}
|
||||
this.classifyingBlocks = true;
|
||||
|
||||
// classification requires an esplora backend
|
||||
if (!Common.gogglesIndexingEnabled() || config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
return;
|
||||
@@ -679,6 +685,8 @@ class Blocks {
|
||||
indexedThisRun = 0;
|
||||
}
|
||||
}
|
||||
|
||||
this.classifyingBlocks = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -776,6 +784,16 @@ class Blocks {
|
||||
} else {
|
||||
this.currentBlockHeight = this.blocks[this.blocks.length - 1].height;
|
||||
}
|
||||
if (this.currentBlockHeight >= 503) {
|
||||
try {
|
||||
const quarterEpochBlockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight - 503);
|
||||
const quarterEpochBlock = await bitcoinApi.$getBlock(quarterEpochBlockHash);
|
||||
this.quarterEpochBlockTime = quarterEpochBlock?.timestamp;
|
||||
} catch (e) {
|
||||
this.quarterEpochBlockTime = null;
|
||||
logger.warn('failed to update last epoch block time: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
if (blockHeightTip - this.currentBlockHeight > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 2) {
|
||||
logger.info(`${blockHeightTip - this.currentBlockHeight} blocks since tip. Fast forwarding to the ${config.MEMPOOL.INITIAL_BLOCKS_AMOUNT} recent blocks`);
|
||||
@@ -975,7 +993,6 @@ class Blocks {
|
||||
await redisCache.$removeTransactions(txIds);
|
||||
await rbfCache.updateCache();
|
||||
}
|
||||
rebroadcaster.remove(txIds);
|
||||
|
||||
handledBlocks++;
|
||||
}
|
||||
@@ -1310,6 +1327,10 @@ class Blocks {
|
||||
return this.previousDifficultyRetarget;
|
||||
}
|
||||
|
||||
public getQuarterEpochBlockTime(): number | null {
|
||||
return this.quarterEpochBlockTime;
|
||||
}
|
||||
|
||||
public getCurrentBlockHeight(): number {
|
||||
return this.currentBlockHeight;
|
||||
}
|
||||
|
||||
@@ -245,6 +245,8 @@ export class Common {
|
||||
flags |= TransactionFlags.v1;
|
||||
} else if (tx.version === 2) {
|
||||
flags |= TransactionFlags.v2;
|
||||
} else if (tx.version === 3) {
|
||||
flags |= TransactionFlags.v3;
|
||||
}
|
||||
const reusedInputAddresses: { [address: string ]: number } = {};
|
||||
const reusedOutputAddresses: { [address: string ]: number } = {};
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface DifficultyAdjustment {
|
||||
previousTime: number; // Unix time in ms
|
||||
nextRetargetHeight: number; // Block Height
|
||||
timeAvg: number; // Duration of time in ms
|
||||
adjustedTimeAvg; // Expected block interval with hashrate implied over last 504 blocks
|
||||
timeOffset: number; // (Testnet) Time since last block (cap @ 20min) in ms
|
||||
expectedBlocks: number; // Block count
|
||||
}
|
||||
@@ -80,6 +81,7 @@ export function calcBitsDifference(oldBits: number, newBits: number): number {
|
||||
|
||||
export function calcDifficultyAdjustment(
|
||||
DATime: number,
|
||||
quarterEpochTime: number | null,
|
||||
nowSeconds: number,
|
||||
blockHeight: number,
|
||||
previousRetarget: number,
|
||||
@@ -100,8 +102,20 @@ export function calcDifficultyAdjustment(
|
||||
|
||||
let difficultyChange = 0;
|
||||
let timeAvgSecs = blocksInEpoch ? diffSeconds / blocksInEpoch : BLOCK_SECONDS_TARGET;
|
||||
let adjustedTimeAvgSecs = timeAvgSecs;
|
||||
|
||||
// for the first 504 blocks of the epoch, calculate the expected avg block interval
|
||||
// from a sliding window over the last 504 blocks
|
||||
if (quarterEpochTime && blocksInEpoch < 503) {
|
||||
const timeLastEpoch = DATime - quarterEpochTime;
|
||||
const adjustedTimeLastEpoch = timeLastEpoch * (1 + (previousRetarget / 100));
|
||||
const adjustedTimeSpan = diffSeconds + adjustedTimeLastEpoch;
|
||||
adjustedTimeAvgSecs = adjustedTimeSpan / 503;
|
||||
difficultyChange = (BLOCK_SECONDS_TARGET / (adjustedTimeSpan / 504) - 1) * 100;
|
||||
} else {
|
||||
difficultyChange = (BLOCK_SECONDS_TARGET / (actualTimespan / (blocksInEpoch + 1)) - 1) * 100;
|
||||
}
|
||||
|
||||
difficultyChange = (BLOCK_SECONDS_TARGET / (actualTimespan / (blocksInEpoch + 1)) - 1) * 100;
|
||||
// Max increase is x4 (+300%)
|
||||
if (difficultyChange > 300) {
|
||||
difficultyChange = 300;
|
||||
@@ -126,7 +140,8 @@ export function calcDifficultyAdjustment(
|
||||
}
|
||||
|
||||
const timeAvg = Math.floor(timeAvgSecs * 1000);
|
||||
const remainingTime = remainingBlocks * timeAvg;
|
||||
const adjustedTimeAvg = Math.floor(adjustedTimeAvgSecs * 1000);
|
||||
const remainingTime = remainingBlocks * adjustedTimeAvg;
|
||||
const estimatedRetargetDate = remainingTime + nowSeconds * 1000;
|
||||
|
||||
return {
|
||||
@@ -139,6 +154,7 @@ export function calcDifficultyAdjustment(
|
||||
previousTime: DATime,
|
||||
nextRetargetHeight,
|
||||
timeAvg,
|
||||
adjustedTimeAvg,
|
||||
timeOffset,
|
||||
expectedBlocks,
|
||||
};
|
||||
@@ -155,9 +171,10 @@ class DifficultyAdjustmentApi {
|
||||
return null;
|
||||
}
|
||||
const nowSeconds = Math.floor(new Date().getTime() / 1000);
|
||||
const quarterEpochBlockTime = blocks.getQuarterEpochBlockTime();
|
||||
|
||||
return calcDifficultyAdjustment(
|
||||
DATime, nowSeconds, blockHeight, previousRetarget,
|
||||
DATime, quarterEpochBlockTime, nowSeconds, blockHeight, previousRetarget,
|
||||
config.MEMPOOL.NETWORK, latestBlock.timestamp
|
||||
);
|
||||
}
|
||||
|
||||
@@ -398,39 +398,24 @@ class ElementsParser {
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Get all of the federation addresses one month ago, most balances first
|
||||
public async $getFederationAddressesOneMonthAgo(): Promise<any> {
|
||||
const query = `
|
||||
SELECT COUNT(*) AS addresses_count_one_month FROM (
|
||||
SELECT bitcoinaddress, SUM(amount) AS balance
|
||||
FROM federation_txos
|
||||
WHERE
|
||||
(blocktime < UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP())))
|
||||
AND
|
||||
((unspent = 1) OR (unspent = 0 AND lasttimeupdate > UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP()))))
|
||||
GROUP BY bitcoinaddress
|
||||
) AS result;`;
|
||||
// Get the total number of federation addresses
|
||||
public async $getFederationAddressesNumber(): Promise<any> {
|
||||
const query = `SELECT COUNT(DISTINCT bitcoinaddress) AS address_count FROM federation_txos WHERE unspent = 1;`;
|
||||
const [rows] = await DB.query(query);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
// Get all of the UTXOs held by the federation one month ago, most recent first
|
||||
public async $getFederationUtxosOneMonthAgo(): Promise<any> {
|
||||
const query = `
|
||||
SELECT COUNT(*) AS utxos_count_one_month FROM federation_txos
|
||||
WHERE
|
||||
(blocktime < UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP())))
|
||||
AND
|
||||
((unspent = 1) OR (unspent = 0 AND lasttimeupdate > UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP()))))
|
||||
ORDER BY blocktime DESC;`;
|
||||
// Get the total number of federation utxos
|
||||
public async $getFederationUtxosNumber(): Promise<any> {
|
||||
const query = `SELECT COUNT(*) AS utxo_count FROM federation_txos WHERE unspent = 1;`;
|
||||
const [rows] = await DB.query(query);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
// Get recent pegouts from the federation (3 months old)
|
||||
public async $getRecentPegouts(): Promise<any> {
|
||||
const query = `SELECT txid, txindex, amount, bitcoinaddress, bitcointxid, bitcoinindex, datetime AS blocktime FROM elements_pegs WHERE amount < 0 AND datetime > UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -90, CURRENT_TIMESTAMP())) ORDER BY blocktime;`;
|
||||
const [rows] = await DB.query(query);
|
||||
// Get recent pegs in / out
|
||||
public async $getPegsList(count: number = 0): Promise<any> {
|
||||
const query = `SELECT txid, txindex, amount, bitcoinaddress, bitcointxid, bitcoinindex, datetime AS blocktime FROM elements_pegs ORDER BY block DESC LIMIT 15 OFFSET ?;`;
|
||||
const [rows] = await DB.query(query, [count]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
@@ -443,6 +428,12 @@ class ElementsParser {
|
||||
pegOutQuery[0][0]
|
||||
];
|
||||
}
|
||||
|
||||
// Get the total pegs number
|
||||
public async $getPegsCount(): Promise<any> {
|
||||
const [rows] = await DB.query(`SELECT COUNT(*) AS pegs_count FROM elements_pegs;`);
|
||||
return rows[0];
|
||||
}
|
||||
}
|
||||
|
||||
export default new ElementsParser();
|
||||
|
||||
@@ -17,14 +17,15 @@ class LiquidRoutes {
|
||||
app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs', this.$getElementsPegs)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', this.$getElementsPegsByMonth)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/list/:count', this.$getPegsList)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/volume', this.$getPegsVolumeDaily)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/count', this.$getPegsCount)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves', this.$getFederationReserves)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/month', this.$getFederationReservesByMonth)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegouts', this.$getPegOuts)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/addresses', this.$getFederationAddresses)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/addresses/previous-month', this.$getFederationAddressesOneMonthAgo)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/addresses/total', this.$getFederationAddressesNumber)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos', this.$getFederationUtxos)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos/previous-month', this.$getFederationUtxosOneMonthAgo)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos/total', this.$getFederationUtxosNumber)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/status', this.$getFederationAuditStatus)
|
||||
;
|
||||
}
|
||||
@@ -142,12 +143,12 @@ class LiquidRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async $getFederationAddressesOneMonthAgo(req: Request, res: Response) {
|
||||
private async $getFederationAddressesNumber(req: Request, res: Response) {
|
||||
try {
|
||||
const federationAddresses = await elementsParser.$getFederationAddressesOneMonthAgo();
|
||||
const federationAddresses = await elementsParser.$getFederationAddressesNumber();
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60 * 24).toUTCString());
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(federationAddresses);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
@@ -166,25 +167,25 @@ class LiquidRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async $getFederationUtxosOneMonthAgo(req: Request, res: Response) {
|
||||
private async $getFederationUtxosNumber(req: Request, res: Response) {
|
||||
try {
|
||||
const federationUtxos = await elementsParser.$getFederationUtxosOneMonthAgo();
|
||||
const federationUtxos = await elementsParser.$getFederationUtxosNumber();
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60 * 24).toUTCString());
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(federationUtxos);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getPegOuts(req: Request, res: Response) {
|
||||
private async $getPegsList(req: Request, res: Response) {
|
||||
try {
|
||||
const recentPegOuts = await elementsParser.$getRecentPegouts();
|
||||
const recentPegs = await elementsParser.$getPegsList(parseInt(req.params?.count));
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(recentPegOuts);
|
||||
res.json(recentPegs);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
@@ -202,6 +203,18 @@ class LiquidRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async $getPegsCount(req: Request, res: Response) {
|
||||
try {
|
||||
const pegsCount = await elementsParser.$getPegsCount();
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(pegsCount);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new LiquidRoutes();
|
||||
|
||||
@@ -6,7 +6,6 @@ import config from '../config';
|
||||
import { Worker } from 'worker_threads';
|
||||
import path from 'path';
|
||||
import mempool from './mempool';
|
||||
import rebroadcaster from './rebroadcaster';
|
||||
|
||||
const MAX_UINT32 = Math.pow(2, 32) - 1;
|
||||
|
||||
@@ -113,7 +112,6 @@ class MempoolBlocks {
|
||||
let blockWeight = 0;
|
||||
let blockVsize = 0;
|
||||
let blockFees = 0;
|
||||
const purgeRate = mempool.getMempoolInfo().mempoolminfee * 100000;
|
||||
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
||||
let transactionIds: string[] = [];
|
||||
let transactions: MempoolTransactionExtended[] = [];
|
||||
@@ -159,16 +157,6 @@ class MempoolBlocks {
|
||||
transactionIds = [tx.txid];
|
||||
transactions = [tx];
|
||||
}
|
||||
|
||||
if (tx.purged) {
|
||||
if (tx.effectiveFeePerVsize >= purgeRate) {
|
||||
rebroadcaster.unpurge(tx.txid);
|
||||
tx.purged = false;
|
||||
}
|
||||
} else if (tx.effectiveFeePerVsize < purgeRate) {
|
||||
rebroadcaster.purge(tx.txid);
|
||||
tx.purged = true;
|
||||
}
|
||||
});
|
||||
if (transactions.length) {
|
||||
const feeStats = onlineStats ? feeStatsCalculator.getRawFeeStats() : undefined;
|
||||
@@ -464,12 +452,12 @@ class MempoolBlocks {
|
||||
}
|
||||
}
|
||||
|
||||
private processBlockTemplates(mempoolTxs: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], accelerations, accelerationPool, saveResults): MempoolBlockWithTransactions[] {
|
||||
private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], accelerations, accelerationPool, saveResults): MempoolBlockWithTransactions[] {
|
||||
for (const [txid, rate] of rates) {
|
||||
if (txid in mempoolTxs) {
|
||||
mempoolTxs[txid].cpfpDirty = (rate !== mempoolTxs[txid].effectiveFeePerVsize);
|
||||
mempoolTxs[txid].effectiveFeePerVsize = rate;
|
||||
mempoolTxs[txid].cpfpChecked = false;
|
||||
if (txid in mempool) {
|
||||
mempool[txid].cpfpDirty = (rate !== mempool[txid].effectiveFeePerVsize);
|
||||
mempool[txid].effectiveFeePerVsize = rate;
|
||||
mempool[txid].cpfpChecked = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,7 +469,7 @@ class MempoolBlocks {
|
||||
if (blockWeights && blockWeights[7] !== null) {
|
||||
stackWeight = blockWeights[7];
|
||||
} else {
|
||||
stackWeight = blocks[lastBlockIndex].reduce((total, tx) => total + (mempoolTxs[tx]?.weight || 0), 0);
|
||||
stackWeight = blocks[lastBlockIndex].reduce((total, tx) => total + (mempool[tx]?.weight || 0), 0);
|
||||
}
|
||||
hasBlockStack = stackWeight > config.MEMPOOL.BLOCK_WEIGHT_UNITS;
|
||||
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]);
|
||||
@@ -489,7 +477,7 @@ class MempoolBlocks {
|
||||
|
||||
for (const cluster of clusters) {
|
||||
for (const memberTxid of cluster) {
|
||||
const mempoolTx = mempoolTxs[memberTxid];
|
||||
const mempoolTx = mempool[memberTxid];
|
||||
if (mempoolTx) {
|
||||
const ancestors: Ancestor[] = [];
|
||||
const descendants: Ancestor[] = [];
|
||||
@@ -500,12 +488,12 @@ class MempoolBlocks {
|
||||
} else {
|
||||
const relative = {
|
||||
txid: txid,
|
||||
fee: mempoolTxs[txid].fee,
|
||||
weight: (mempoolTxs[txid].adjustedVsize * 4),
|
||||
fee: mempool[txid].fee,
|
||||
weight: (mempool[txid].adjustedVsize * 4),
|
||||
};
|
||||
if (matched) {
|
||||
descendants.push(relative);
|
||||
mempoolTx.lastBoosted = Math.max(mempoolTx.lastBoosted || 0, mempoolTxs[txid].firstSeen || 0);
|
||||
mempoolTx.lastBoosted = Math.max(mempoolTx.lastBoosted || 0, mempool[txid].firstSeen || 0);
|
||||
} else {
|
||||
ancestors.push(relative);
|
||||
}
|
||||
@@ -520,7 +508,6 @@ class MempoolBlocks {
|
||||
}
|
||||
|
||||
const isAccelerated : { [txid: string]: boolean } = {};
|
||||
const purgeRate = mempool.getMempoolInfo().mempoolminfee * 100000;
|
||||
|
||||
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
||||
// update this thread's mempool with the results
|
||||
@@ -533,7 +520,7 @@ class MempoolBlocks {
|
||||
const transactions: MempoolTransactionExtended[] = [];
|
||||
for (const txid of block) {
|
||||
if (txid) {
|
||||
mempoolTx = mempoolTxs[txid];
|
||||
mempoolTx = mempool[txid];
|
||||
// save position in projected blocks
|
||||
mempoolTx.position = {
|
||||
block: blockIndex,
|
||||
@@ -557,10 +544,10 @@ class MempoolBlocks {
|
||||
}
|
||||
mempoolTx.acceleration = true;
|
||||
for (const ancestor of mempoolTx.ancestors || []) {
|
||||
if (!mempoolTxs[ancestor.txid].acceleration) {
|
||||
mempoolTxs[ancestor.txid].cpfpDirty = true;
|
||||
if (!mempool[ancestor.txid].acceleration) {
|
||||
mempool[ancestor.txid].cpfpDirty = true;
|
||||
}
|
||||
mempoolTxs[ancestor.txid].acceleration = true;
|
||||
mempool[ancestor.txid].acceleration = true;
|
||||
isAccelerated[ancestor.txid] = true;
|
||||
}
|
||||
} else {
|
||||
@@ -575,17 +562,6 @@ class MempoolBlocks {
|
||||
feeStatsCalculator.processNext(mempoolTx);
|
||||
}
|
||||
|
||||
// update purge status
|
||||
if (mempoolTx.purged) {
|
||||
if (mempoolTx.effectiveFeePerVsize >= purgeRate) {
|
||||
rebroadcaster.unpurge(mempoolTx.txid);
|
||||
mempoolTx.purged = false;
|
||||
}
|
||||
} else if (mempoolTx.effectiveFeePerVsize < purgeRate) {
|
||||
rebroadcaster.purge(mempoolTx.txid);
|
||||
mempoolTx.purged = true;
|
||||
}
|
||||
|
||||
totalSize += mempoolTx.size;
|
||||
totalVsize += mempoolTx.vsize;
|
||||
totalWeight += mempoolTx.weight;
|
||||
|
||||
@@ -11,7 +11,6 @@ import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||
import rbfCache from './rbf-cache';
|
||||
import { Acceleration } from './services/acceleration';
|
||||
import redisCache from './redis-cache';
|
||||
import rebroadcaster from './rebroadcaster';
|
||||
|
||||
class Mempool {
|
||||
private inSync: boolean = false;
|
||||
@@ -362,7 +361,6 @@ class Mempool {
|
||||
await redisCache.$removeTransactions(deletedTransactions.map(tx => tx.txid));
|
||||
await rbfCache.updateCache();
|
||||
}
|
||||
rebroadcaster.remove(deletedTransactions.map(tx => tx.txid));
|
||||
|
||||
const end = new Date().getTime();
|
||||
const time = end - start;
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
import config from '../config';
|
||||
import logger from '../logger';
|
||||
import { MempoolTransactionExtended } from '../mempool.interfaces';
|
||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||
import bitcoinClient from './bitcoin/bitcoin-client';
|
||||
import mempool from './mempool';
|
||||
import mempoolBlocks from './mempool-blocks';
|
||||
|
||||
/**
|
||||
* Transaction Rebroadcaster
|
||||
*
|
||||
* Automatically rebroadcasts transactions from near the top of the mempool which peers may not know about.
|
||||
*
|
||||
* e.g:
|
||||
* - transactions older than the default mempoolexpiry (336 hours)
|
||||
* - transactions which previously fell below the default maxmempool purge rate
|
||||
* - transactions we observed to be unexpectedly missing from recent mined blocks
|
||||
*
|
||||
* To avoid spamming relay peers, rebroadcasting is probabilistic, based on the target frequency
|
||||
* set in config.REBROADCAST.FREQUENCY and a "priority" derived from the reason for rebroadcast.
|
||||
*/
|
||||
|
||||
class Rebroadcaster {
|
||||
private unpurged = new Set<string>();
|
||||
private rebroadcasted = new Set<string>();
|
||||
private missing = new Set<string>();
|
||||
private lastRun = (Date.now() / 1000);
|
||||
|
||||
async $run(): Promise<void> {
|
||||
if (!config.REBROADCAST.ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now() / 1000;
|
||||
const transactions = mempool.getMempool();
|
||||
const blocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
const toRebroadcast: { txid: string, priority: number }[] = [];
|
||||
|
||||
const twoWeeksAgo = now - (14 * 24 * 60 * 60);
|
||||
for (const block of blocks) {
|
||||
for (const txid of block.transactionIds) {
|
||||
const tx = transactions[txid];
|
||||
if (tx && this.isRebroadcastable(tx, twoWeeksAgo)) {
|
||||
if (this.unpurged.has(tx.txid) || this.missing.has(tx.txid)) {
|
||||
toRebroadcast.push({ txid: tx.txid, priority: 1 });
|
||||
} else {
|
||||
const depth = (tx.position?.block || 0) * (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) + (tx.position?.vsize || 0);
|
||||
// priority approaches 0.5 as mempool depth approaches zero
|
||||
// scaling factor ensures all txs in the next block have priority >= 0.4
|
||||
const priority = 0.5 / (1 + (depth / (config.MEMPOOL.BLOCK_WEIGHT_UNITS * 2)));
|
||||
toRebroadcast.push({ txid: tx.txid, priority });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const elapsed = now - this.lastRun;
|
||||
// config.REBROADCAST.FREQUENCY is actually the target /period/
|
||||
const probabilityFactor = elapsed / config.REBROADCAST.FREQUENCY;
|
||||
|
||||
let totalRebroadcast = 0;
|
||||
let totalFailed = 0;
|
||||
for (const tx of toRebroadcast) {
|
||||
// rebroadcast with probability = priority * frequency / number of txs
|
||||
const cluster = this.getAncestors(tx.txid, transactions, twoWeeksAgo);
|
||||
if (Math.random() < (tx.priority * probabilityFactor / cluster.length)) {
|
||||
for (const txid of cluster) {
|
||||
if (await this.$rebroadcastTx(txid)) {
|
||||
totalRebroadcast++;
|
||||
} else {
|
||||
totalFailed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.lastRun = (Date.now() / 1000);
|
||||
logger.debug(`${toRebroadcast.length - totalRebroadcast} candidates, ${totalRebroadcast + totalFailed} attempted, ${totalRebroadcast} successful`, logger.tags.rebroadcaster);
|
||||
}
|
||||
|
||||
// allow rebroadcast of old, missing or previously purged transactions
|
||||
// within the first 7 projected blocks, that haven't been rebroadcast before
|
||||
private isRebroadcastable(tx: MempoolTransactionExtended, minAge: number): boolean {
|
||||
return !!(tx.firstSeen
|
||||
&& tx.position
|
||||
&& tx.position.block < 7
|
||||
&& ((tx.firstSeen < minAge) || this.unpurged.has(tx.txid) || this.missing.has(tx.txid))
|
||||
&& !this.rebroadcasted.has(tx.txid)
|
||||
);
|
||||
}
|
||||
|
||||
private async $rebroadcastTx(txid: string): Promise<boolean> {
|
||||
try {
|
||||
const hex = await bitcoinApi.$getTransactionHex(txid);
|
||||
if (hex) {
|
||||
const txidResult = await bitcoinClient.sendRawTransaction(hex);
|
||||
if (txidResult) {
|
||||
this.rebroadcasted.add(txid);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('Failed to rebroadcast transaction: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// find and return a list of rebroadcastable ancestors of the given txid (including itself)
|
||||
private getAncestors(txid: string, transactions: { [txid: string]: MempoolTransactionExtended }, minAge: number): string[] {
|
||||
const ancestors = new Set<string>();
|
||||
const skip = new Set<string>();
|
||||
const stack: string[] = [txid];
|
||||
let sanityBreak = 0;
|
||||
while (stack.length && sanityBreak < 100) {
|
||||
const nextTxid = stack.pop();
|
||||
if (nextTxid) {
|
||||
ancestors.add(nextTxid);
|
||||
for (const vin of transactions[nextTxid].vin) {
|
||||
if ( !skip.has(nextTxid)
|
||||
&& !ancestors.has(nextTxid)
|
||||
&& transactions[nextTxid]
|
||||
&& this.isRebroadcastable(transactions[nextTxid], minAge)
|
||||
) {
|
||||
stack.push(vin.txid);
|
||||
} else {
|
||||
skip.add(nextTxid);
|
||||
}
|
||||
}
|
||||
}
|
||||
sanityBreak++;
|
||||
}
|
||||
return [...ancestors.keys()].reverse();
|
||||
}
|
||||
|
||||
// transaction re-entered default mempools
|
||||
public unpurge(txid: string): void {
|
||||
if (!config.REBROADCAST.ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.unpurged.add(txid);
|
||||
}
|
||||
|
||||
// transaction was purged from default mempools
|
||||
public purge(txid: string): void {
|
||||
if (!config.REBROADCAST.ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.unpurged.delete(txid);
|
||||
this.missing.delete(txid);
|
||||
this.rebroadcasted.delete(txid);
|
||||
}
|
||||
|
||||
// transactions were unexpectedly missing from a block
|
||||
public missed(txids: string[]): void {
|
||||
if (!config.REBROADCAST.ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const txid of txids) {
|
||||
this.missing.add(txid);
|
||||
}
|
||||
}
|
||||
|
||||
// transactions were evicted or mined
|
||||
public remove(txids: string[]): void {
|
||||
if (!config.REBROADCAST.ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const txid of txids) {
|
||||
this.unpurged.delete(txid);
|
||||
this.missing.delete(txid);
|
||||
this.rebroadcasted.delete(txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Rebroadcaster();
|
||||
@@ -19,45 +19,90 @@ class RedisCache {
|
||||
private client;
|
||||
private connected = false;
|
||||
private schemaVersion = 1;
|
||||
private redisConfig: any;
|
||||
|
||||
private pauseFlush: boolean = false;
|
||||
private cacheQueue: MempoolTransactionExtended[] = [];
|
||||
private removeQueue: string[] = [];
|
||||
private rbfCacheQueue: { type: string, txid: string, value: any }[] = [];
|
||||
private rbfRemoveQueue: { type: string, txid: string }[] = [];
|
||||
private txFlushLimit: number = 10000;
|
||||
|
||||
constructor() {
|
||||
if (config.REDIS.ENABLED) {
|
||||
const redisConfig = {
|
||||
this.redisConfig = {
|
||||
socket: {
|
||||
path: config.REDIS.UNIX_SOCKET_PATH
|
||||
},
|
||||
database: NetworkDB[config.MEMPOOL.NETWORK],
|
||||
};
|
||||
this.client = createClient(redisConfig);
|
||||
this.client.on('error', (e) => {
|
||||
logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
|
||||
});
|
||||
this.$ensureConnected();
|
||||
setInterval(() => { this.$ensureConnected(); }, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
private async $ensureConnected(): Promise<void> {
|
||||
private async $ensureConnected(): Promise<boolean> {
|
||||
if (!this.connected && config.REDIS.ENABLED) {
|
||||
return this.client.connect().then(async () => {
|
||||
this.connected = true;
|
||||
logger.info(`Redis client connected`);
|
||||
const version = await this.client.get('schema_version');
|
||||
if (version !== this.schemaVersion) {
|
||||
// schema changed
|
||||
// perform migrations or flush DB if necessary
|
||||
logger.info(`Redis schema version changed from ${version} to ${this.schemaVersion}`);
|
||||
await this.client.set('schema_version', this.schemaVersion);
|
||||
}
|
||||
});
|
||||
try {
|
||||
this.client = createClient(this.redisConfig);
|
||||
this.client.on('error', async (e) => {
|
||||
logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
|
||||
this.connected = false;
|
||||
await this.client.disconnect();
|
||||
});
|
||||
await this.client.connect().then(async () => {
|
||||
try {
|
||||
const version = await this.client.get('schema_version');
|
||||
this.connected = true;
|
||||
if (version !== this.schemaVersion) {
|
||||
// schema changed
|
||||
// perform migrations or flush DB if necessary
|
||||
logger.info(`Redis schema version changed from ${version} to ${this.schemaVersion}`);
|
||||
await this.client.set('schema_version', this.schemaVersion);
|
||||
}
|
||||
logger.info(`Redis client connected`);
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.connected = false;
|
||||
logger.warn('Failed to connect to Redis');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
await this.$onConnected();
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.warn('Error connecting to Redis: ' + (e instanceof Error ? e.message : e));
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
// test connection
|
||||
await this.client.get('schema_version');
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.warn('Lost connection to Redis: ' + (e instanceof Error ? e.message : e));
|
||||
logger.warn('Attempting to reconnect in 10 seconds');
|
||||
this.connected = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async $updateBlocks(blocks: BlockExtended[]) {
|
||||
private async $onConnected(): Promise<void> {
|
||||
await this.$flushTransactions();
|
||||
await this.$removeTransactions([]);
|
||||
await this.$flushRbfQueues();
|
||||
}
|
||||
|
||||
async $updateBlocks(blocks: BlockExtended[]): Promise<void> {
|
||||
if (!config.REDIS.ENABLED) {
|
||||
return;
|
||||
}
|
||||
if (!this.connected) {
|
||||
logger.warn(`Failed to update blocks in Redis cache: Redis is not connected`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
await this.client.set('blocks', JSON.stringify(blocks));
|
||||
logger.debug(`Saved latest blocks to Redis cache`);
|
||||
} catch (e) {
|
||||
@@ -65,9 +110,15 @@ class RedisCache {
|
||||
}
|
||||
}
|
||||
|
||||
async $updateBlockSummaries(summaries: BlockSummary[]) {
|
||||
async $updateBlockSummaries(summaries: BlockSummary[]): Promise<void> {
|
||||
if (!config.REDIS.ENABLED) {
|
||||
return;
|
||||
}
|
||||
if (!this.connected) {
|
||||
logger.warn(`Failed to update block summaries in Redis cache: Redis is not connected`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
await this.client.set('block-summaries', JSON.stringify(summaries));
|
||||
logger.debug(`Saved latest block summaries to Redis cache`);
|
||||
} catch (e) {
|
||||
@@ -75,30 +126,35 @@ class RedisCache {
|
||||
}
|
||||
}
|
||||
|
||||
async $addTransaction(tx: MempoolTransactionExtended) {
|
||||
async $addTransaction(tx: MempoolTransactionExtended): Promise<void> {
|
||||
if (!config.REDIS.ENABLED) {
|
||||
return;
|
||||
}
|
||||
this.cacheQueue.push(tx);
|
||||
if (this.cacheQueue.length >= this.txFlushLimit) {
|
||||
await this.$flushTransactions();
|
||||
if (!this.pauseFlush) {
|
||||
await this.$flushTransactions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async $flushTransactions() {
|
||||
const success = await this.$addTransactions(this.cacheQueue);
|
||||
if (success) {
|
||||
logger.debug(`Saved ${this.cacheQueue.length} transactions to Redis cache`);
|
||||
this.cacheQueue = [];
|
||||
} else {
|
||||
logger.err(`Failed to save ${this.cacheQueue.length} transactions to Redis cache`);
|
||||
async $flushTransactions(): Promise<void> {
|
||||
if (!config.REDIS.ENABLED) {
|
||||
return;
|
||||
}
|
||||
if (!this.cacheQueue.length) {
|
||||
return;
|
||||
}
|
||||
if (!this.connected) {
|
||||
logger.warn(`Failed to add ${this.cacheQueue.length} transactions to Redis cache: Redis not connected`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async $addTransactions(newTransactions: MempoolTransactionExtended[]): Promise<boolean> {
|
||||
if (!newTransactions.length) {
|
||||
return true;
|
||||
}
|
||||
this.pauseFlush = false;
|
||||
|
||||
const toAdd = this.cacheQueue.slice(0, this.txFlushLimit);
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const msetData = newTransactions.map(tx => {
|
||||
const msetData = toAdd.map(tx => {
|
||||
const minified: any = { ...tx };
|
||||
delete minified.hex;
|
||||
for (const vin of minified.vin) {
|
||||
@@ -112,30 +168,53 @@ class RedisCache {
|
||||
return [`mempool:tx:${tx.txid}`, JSON.stringify(minified)];
|
||||
});
|
||||
await this.client.MSET(msetData);
|
||||
return true;
|
||||
// successful, remove transactions from cache queue
|
||||
this.cacheQueue = this.cacheQueue.slice(toAdd.length);
|
||||
logger.debug(`Saved ${toAdd.length} transactions to Redis cache, ${this.cacheQueue.length} left in queue`);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to add ${newTransactions.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
return false;
|
||||
logger.warn(`Failed to add ${toAdd.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
this.pauseFlush = true;
|
||||
}
|
||||
}
|
||||
|
||||
async $removeTransactions(transactions: string[]) {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
async $removeTransactions(transactions: string[]): Promise<void> {
|
||||
if (!config.REDIS.ENABLED) {
|
||||
return;
|
||||
}
|
||||
const toRemove = this.removeQueue.concat(transactions);
|
||||
this.removeQueue = [];
|
||||
let failed: string[] = [];
|
||||
let numRemoved = 0;
|
||||
if (this.connected) {
|
||||
const sliceLength = config.REDIS.BATCH_QUERY_BASE_SIZE;
|
||||
for (let i = 0; i < Math.ceil(transactions.length / sliceLength); i++) {
|
||||
const slice = transactions.slice(i * sliceLength, (i + 1) * sliceLength);
|
||||
await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
|
||||
logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
|
||||
for (let i = 0; i < Math.ceil(toRemove.length / sliceLength); i++) {
|
||||
const slice = toRemove.slice(i * sliceLength, (i + 1) * sliceLength);
|
||||
try {
|
||||
await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
|
||||
numRemoved+= sliceLength;
|
||||
logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to remove ${slice.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
failed = failed.concat(slice);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to remove ${transactions.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
// concat instead of replace, in case more txs have been added in the meantime
|
||||
this.removeQueue = this.removeQueue.concat(failed);
|
||||
} else {
|
||||
this.removeQueue = this.removeQueue.concat(toRemove);
|
||||
}
|
||||
}
|
||||
|
||||
async $setRbfEntry(type: string, txid: string, value: any): Promise<void> {
|
||||
if (!config.REDIS.ENABLED) {
|
||||
return;
|
||||
}
|
||||
if (!this.connected) {
|
||||
this.rbfCacheQueue.push({ type, txid, value });
|
||||
logger.warn(`Failed to set RBF ${type} in Redis cache: Redis is not connected`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
await this.client.set(`rbf:${type}:${txid}`, JSON.stringify(value));
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to set RBF ${type} in Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
@@ -143,17 +222,55 @@ class RedisCache {
|
||||
}
|
||||
|
||||
async $removeRbfEntry(type: string, txid: string): Promise<void> {
|
||||
if (!config.REDIS.ENABLED) {
|
||||
return;
|
||||
}
|
||||
if (!this.connected) {
|
||||
this.rbfRemoveQueue.push({ type, txid });
|
||||
logger.warn(`Failed to remove RBF ${type} from Redis cache: Redis is not connected`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
await this.client.unlink(`rbf:${type}:${txid}`);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to remove RBF ${type} from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
}
|
||||
|
||||
async $getBlocks(): Promise<BlockExtended[]> {
|
||||
private async $flushRbfQueues(): Promise<void> {
|
||||
if (!config.REDIS.ENABLED) {
|
||||
return;
|
||||
}
|
||||
if (!this.connected) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const toAdd = this.rbfCacheQueue;
|
||||
this.rbfCacheQueue = [];
|
||||
for (const { type, txid, value } of toAdd) {
|
||||
await this.$setRbfEntry(type, txid, value);
|
||||
}
|
||||
logger.debug(`Saved ${toAdd.length} queued RBF entries to the Redis cache`);
|
||||
const toRemove = this.rbfRemoveQueue;
|
||||
this.rbfRemoveQueue = [];
|
||||
for (const { type, txid } of toRemove) {
|
||||
await this.$removeRbfEntry(type, txid);
|
||||
}
|
||||
logger.debug(`Removed ${toRemove.length} queued RBF entries from the Redis cache`);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to flush RBF cache event queues after reconnecting to Redis: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
}
|
||||
|
||||
async $getBlocks(): Promise<BlockExtended[]> {
|
||||
if (!config.REDIS.ENABLED) {
|
||||
return [];
|
||||
}
|
||||
if (!this.connected) {
|
||||
logger.warn(`Failed to retrieve blocks from Redis cache: Redis is not connected`);
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const json = await this.client.get('blocks');
|
||||
return JSON.parse(json);
|
||||
} catch (e) {
|
||||
@@ -163,8 +280,14 @@ class RedisCache {
|
||||
}
|
||||
|
||||
async $getBlockSummaries(): Promise<BlockSummary[]> {
|
||||
if (!config.REDIS.ENABLED) {
|
||||
return [];
|
||||
}
|
||||
if (!this.connected) {
|
||||
logger.warn(`Failed to retrieve blocks from Redis cache: Redis is not connected`);
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const json = await this.client.get('block-summaries');
|
||||
return JSON.parse(json);
|
||||
} catch (e) {
|
||||
@@ -174,10 +297,16 @@ class RedisCache {
|
||||
}
|
||||
|
||||
async $getMempool(): Promise<{ [txid: string]: MempoolTransactionExtended }> {
|
||||
if (!config.REDIS.ENABLED) {
|
||||
return {};
|
||||
}
|
||||
if (!this.connected) {
|
||||
logger.warn(`Failed to retrieve mempool from Redis cache: Redis is not connected`);
|
||||
return {};
|
||||
}
|
||||
const start = Date.now();
|
||||
const mempool = {};
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const mempoolList = await this.scanKeys<MempoolTransactionExtended>('mempool:tx:*');
|
||||
for (const tx of mempoolList) {
|
||||
mempool[tx.key] = tx.value;
|
||||
@@ -191,8 +320,14 @@ class RedisCache {
|
||||
}
|
||||
|
||||
async $getRbfEntries(type: string): Promise<any[]> {
|
||||
if (!config.REDIS.ENABLED) {
|
||||
return [];
|
||||
}
|
||||
if (!this.connected) {
|
||||
logger.warn(`Failed to retrieve Rbf ${type}s from Redis cache: Redis is not connected`);
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const rbfEntries = await this.scanKeys<MempoolTransactionExtended[]>(`rbf:${type}:*`);
|
||||
return rbfEntries;
|
||||
} catch (e) {
|
||||
@@ -201,7 +336,10 @@ class RedisCache {
|
||||
}
|
||||
}
|
||||
|
||||
async $loadCache() {
|
||||
async $loadCache(): Promise<void> {
|
||||
if (!config.REDIS.ENABLED) {
|
||||
return;
|
||||
}
|
||||
logger.info('Restoring mempool and blocks data from Redis cache');
|
||||
// Load block data
|
||||
const loadedBlocks = await this.$getBlocks();
|
||||
@@ -226,7 +364,7 @@ class RedisCache {
|
||||
});
|
||||
}
|
||||
|
||||
private inflateLoadedTxs(mempool: { [txid: string]: MempoolTransactionExtended }) {
|
||||
private inflateLoadedTxs(mempool: { [txid: string]: MempoolTransactionExtended }): void {
|
||||
for (const tx of Object.values(mempool)) {
|
||||
for (const vin of tx.vin) {
|
||||
if (vin.scriptsig) {
|
||||
|
||||
@@ -129,7 +129,6 @@ class TransactionUtils {
|
||||
feePerVsize: feePerVbytes,
|
||||
adjustedFeePerVsize: adjustedFeePerVsize,
|
||||
effectiveFeePerVsize: adjustedFeePerVsize,
|
||||
purged: false,
|
||||
});
|
||||
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
|
||||
transactionExtended.firstSeen = Math.round((Date.now() / 1000));
|
||||
|
||||
@@ -24,7 +24,6 @@ import { ApiPrice } from '../repositories/PricesRepository';
|
||||
import accelerationApi from './services/acceleration';
|
||||
import mempool from './mempool';
|
||||
import statistics from './statistics/statistics';
|
||||
import rebroadcaster from './rebroadcaster';
|
||||
|
||||
interface AddressTransactions {
|
||||
mempool: MempoolTransactionExtended[],
|
||||
@@ -806,8 +805,6 @@ class WebsocketHandler {
|
||||
block.extras.expectedWeight = totalWeight;
|
||||
block.extras.similarity = similarity;
|
||||
}
|
||||
|
||||
rebroadcaster.missed(censored);
|
||||
}
|
||||
} else if (block.extras) {
|
||||
const mBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
|
||||
@@ -147,10 +147,6 @@ interface IConfig {
|
||||
AUDIT_START_HEIGHT: number;
|
||||
SERVERS: string[];
|
||||
},
|
||||
REBROADCAST: {
|
||||
ENABLED: boolean;
|
||||
FREQUENCY: number;
|
||||
},
|
||||
MEMPOOL_SERVICES: {
|
||||
API: string;
|
||||
ACCELERATIONS: boolean;
|
||||
@@ -307,10 +303,6 @@ const defaults: IConfig = {
|
||||
'AUDIT_START_HEIGHT': 774000,
|
||||
'SERVERS': [],
|
||||
},
|
||||
'REBROADCAST': {
|
||||
'ENABLED': false,
|
||||
'FREQUENCY': 3600,
|
||||
},
|
||||
'MEMPOOL_SERVICES': {
|
||||
'API': '',
|
||||
'ACCELERATIONS': false,
|
||||
@@ -339,7 +331,6 @@ class Config implements IConfig {
|
||||
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
||||
MAXMIND: IConfig['MAXMIND'];
|
||||
REPLICATION: IConfig['REPLICATION'];
|
||||
REBROADCAST: IConfig['REBROADCAST'];
|
||||
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
|
||||
REDIS: IConfig['REDIS'];
|
||||
|
||||
@@ -361,7 +352,6 @@ class Config implements IConfig {
|
||||
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
|
||||
this.MAXMIND = configs.MAXMIND;
|
||||
this.REPLICATION = configs.REPLICATION;
|
||||
this.REBROADCAST = configs.REBROADCAST;
|
||||
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
|
||||
this.REDIS = configs.REDIS;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ import { formatBytes, getBytesUnit } from './utils/format';
|
||||
import redisCache from './api/redis-cache';
|
||||
import accelerationApi from './api/services/acceleration';
|
||||
import bitcoinCoreRoutes from './api/bitcoin/bitcoin-core.routes';
|
||||
import rebroadcaster from './api/rebroadcaster';
|
||||
|
||||
class Server {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
@@ -216,7 +215,6 @@ class Server {
|
||||
}
|
||||
indexer.$run();
|
||||
priceUpdater.$run();
|
||||
rebroadcaster.$run();
|
||||
|
||||
// rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
@@ -185,7 +185,8 @@ class Indexer {
|
||||
await blocks.$generateCPFPDatabase();
|
||||
await blocks.$generateAuditStats();
|
||||
await auditReplicator.$sync();
|
||||
await blocks.$classifyBlocks();
|
||||
// do not wait for classify blocks to finish
|
||||
blocks.$classifyBlocks();
|
||||
} catch (e) {
|
||||
this.indexerRunning = false;
|
||||
logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
|
||||
@@ -36,7 +36,6 @@ class Logger {
|
||||
mining: 'Mining',
|
||||
ln: 'Lightning',
|
||||
goggles: 'Goggles',
|
||||
rebroadcaster: 'Rebroadcaster',
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
|
||||
@@ -107,7 +107,6 @@ export interface MempoolTransactionExtended extends TransactionExtended {
|
||||
inputs?: number[];
|
||||
lastBoosted?: number;
|
||||
cpfpDirty?: boolean;
|
||||
purged: boolean;
|
||||
}
|
||||
|
||||
export interface AuditTransaction {
|
||||
@@ -209,6 +208,7 @@ export const TransactionFlags = {
|
||||
no_rbf: 0b00000010n,
|
||||
v1: 0b00000100n,
|
||||
v2: 0b00001000n,
|
||||
v3: 0b00010000n,
|
||||
// address types
|
||||
p2pk: 0b00000001_00000000n,
|
||||
p2ms: 0b00000010_00000000n,
|
||||
|
||||
@@ -143,10 +143,6 @@
|
||||
"AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__,
|
||||
"SERVERS": __REPLICATION_SERVERS__
|
||||
},
|
||||
"REBROADCAST": {
|
||||
"ENABLED": __REBROADCAST_ENABLED__,
|
||||
"FREQUENCY": __REBROADCAST_FREQUENCY__
|
||||
},
|
||||
"MEMPOOL_SERVICES": {
|
||||
"API": "__MEMPOOL_SERVICES_API__",
|
||||
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
|
||||
|
||||
@@ -144,10 +144,6 @@ __REPLICATION_AUDIT__=${REPLICATION_AUDIT:=true}
|
||||
__REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000}
|
||||
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
|
||||
|
||||
# REBROADCAST
|
||||
__REBROADCAST_ENABLED__=${REBROADCAST_ENABLED:=false}
|
||||
__REBROADCAST_FREQUENCY__=${REBROADCAST_FREQUENCY:=3600}
|
||||
|
||||
# MEMPOOL_SERVICES
|
||||
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""}
|
||||
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
|
||||
@@ -292,10 +288,6 @@ sed -i "s!__REPLICATION_AUDIT__!${__REPLICATION_AUDIT__}!g" mempool-config.json
|
||||
sed -i "s!__REPLICATION_AUDIT_START_HEIGHT__!${__REPLICATION_AUDIT_START_HEIGHT__}!g" mempool-config.json
|
||||
sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.json
|
||||
|
||||
# REBROADCAST
|
||||
sed -i "s!__REBROADCAST_ENABLED__!${__REBROADCAST_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__REBROADCAST_FREQUENCY__!${__REBROADCAST_FREQUENCY__}!g" mempool-config.json
|
||||
|
||||
# MEMPOOL_SERVICES
|
||||
sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
|
||||
|
||||
30
frontend/package-lock.json
generated
30
frontend/package-lock.json
generated
@@ -32,7 +32,7 @@
|
||||
"browserify": "^17.0.0",
|
||||
"clipboard": "^2.0.11",
|
||||
"domino": "^2.1.6",
|
||||
"echarts": "~5.4.3",
|
||||
"echarts": "~5.5.0",
|
||||
"lightweight-charts": "~3.8.0",
|
||||
"ngx-echarts": "~16.2.0",
|
||||
"ngx-infinite-scroll": "^16.0.0",
|
||||
@@ -7783,12 +7783,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/echarts": {
|
||||
"version": "5.4.3",
|
||||
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.4.3.tgz",
|
||||
"integrity": "sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA==",
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz",
|
||||
"integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0",
|
||||
"zrender": "5.4.4"
|
||||
"zrender": "5.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/echarts/node_modules/tslib": {
|
||||
@@ -17319,9 +17319,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zrender": {
|
||||
"version": "5.4.4",
|
||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.4.tgz",
|
||||
"integrity": "sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==",
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz",
|
||||
"integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0"
|
||||
}
|
||||
@@ -22822,12 +22822,12 @@
|
||||
}
|
||||
},
|
||||
"echarts": {
|
||||
"version": "5.4.3",
|
||||
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.4.3.tgz",
|
||||
"integrity": "sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA==",
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz",
|
||||
"integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==",
|
||||
"requires": {
|
||||
"tslib": "2.3.0",
|
||||
"zrender": "5.4.4"
|
||||
"zrender": "5.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
@@ -29869,9 +29869,9 @@
|
||||
}
|
||||
},
|
||||
"zrender": {
|
||||
"version": "5.4.4",
|
||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.4.tgz",
|
||||
"integrity": "sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==",
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz",
|
||||
"integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==",
|
||||
"requires": {
|
||||
"tslib": "2.3.0"
|
||||
},
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"browserify": "^17.0.0",
|
||||
"clipboard": "^2.0.11",
|
||||
"domino": "^2.1.6",
|
||||
"echarts": "~5.4.3",
|
||||
"echarts": "~5.5.0",
|
||||
"lightweight-charts": "~3.8.0",
|
||||
"ngx-echarts": "~16.2.0",
|
||||
"ngx-infinite-scroll": "^16.0.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<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]="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>
|
||||
@@ -13,4 +13,4 @@
|
||||
<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 {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { EnterpriseService } from '../../services/enterprise.service';
|
||||
})
|
||||
export class AboutSponsorsComponent {
|
||||
@Input() host = 'https://mempool.space';
|
||||
@Input() context = 'about';
|
||||
|
||||
constructor(private enterpriseService: EnterpriseService) {
|
||||
}
|
||||
|
||||
@@ -416,7 +416,7 @@
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details.<br>
|
||||
</p>
|
||||
<p>
|
||||
This program incorporates software and other components licensed from third parties. See the full list of <a href="https://mempool.space/3rdpartylicenses.txt">Third-Party Licenses</a> for legal notices from those projects.
|
||||
This program incorporates software and other components licensed from third parties. See the full list of <a href="/3rdpartylicenses.txt">Third-Party Licenses</a> for legal notices from those projects.
|
||||
</p>
|
||||
<div class="title">
|
||||
Trademark Notice<br>
|
||||
@@ -429,10 +429,6 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer-links">
|
||||
<a href="/3rdpartylicenses.txt">Third-party Licenses</a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
</tr>
|
||||
<tr class="info">
|
||||
<td class="info">
|
||||
<i><small>mempool.space fee</small></i>
|
||||
<i><small>Accelerator Service Fee</small></i>
|
||||
</td>
|
||||
<td class="amt">
|
||||
+{{ estimate.mempoolBaseFee | number }}
|
||||
@@ -141,7 +141,7 @@
|
||||
</tr>
|
||||
<tr class="info group-last">
|
||||
<td class="info">
|
||||
<i><small>Transaction vsize fee</small></i>
|
||||
<i><small>Transaction Size Surcharge</small></i>
|
||||
</td>
|
||||
<td class="amt">
|
||||
+{{ estimate.vsizeFee | number }}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-ty
|
||||
import { hexToColor } from './utils';
|
||||
import BlockScene from './block-scene';
|
||||
import { TransactionStripped } from '../../interfaces/node-api.interface';
|
||||
import { TransactionFlags } from '../../shared/filters.utils';
|
||||
|
||||
const hoverTransitionTime = 300;
|
||||
const defaultHoverColor = hexToColor('1bd8f4');
|
||||
@@ -58,7 +59,7 @@ export default class TxView implements TransactionStripped {
|
||||
this.acc = tx.acc;
|
||||
this.rate = tx.rate;
|
||||
this.status = tx.status;
|
||||
this.bigintFlags = tx.flags ? BigInt(tx.flags) : 0n;
|
||||
this.bigintFlags = tx.flags ? (BigInt(tx.flags) ^ (this.acc ? TransactionFlags.acceleration : 0n)): 0n;
|
||||
this.initialised = false;
|
||||
this.vertexArray = scene.vertexArray;
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
text-align: left;
|
||||
min-width: 320px;
|
||||
pointer-events: none;
|
||||
z-index: 11;
|
||||
|
||||
&.clickable {
|
||||
pointer-events: all;
|
||||
|
||||
@@ -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}">
|
||||
@@ -51,7 +48,7 @@
|
||||
<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.timeAvg + now" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
|
||||
<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">
|
||||
|
||||
@@ -16,6 +16,7 @@ interface EpochProgress {
|
||||
blocksUntilHalving: number;
|
||||
timeUntilHalving: number;
|
||||
timeAvg: number;
|
||||
adjustedTimeAvg: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -85,6 +86,7 @@ export class DifficultyMiningComponent implements OnInit {
|
||||
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;
|
||||
})
|
||||
|
||||
@@ -78,10 +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 class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-audit">
|
||||
<a class="nav-link" [routerLink]="['/audit']" (click)="collapse()"><fa-icon [icon]="['fas', 'scale-balanced']" [fixedWidth]="true" i18n-title="master-page.btc-reserves-audit" title="BTC Reserves Audit"></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">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div [ngClass]="{'widget': widget}">
|
||||
<div [ngClass]="{'widget': widget, 'extra-margin-right': widget}">
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
|
||||
@@ -4,13 +4,22 @@
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
.extra-margin-right {
|
||||
@media (max-width: 380px) {
|
||||
margin-left: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.65rem !important;
|
||||
padding-bottom: 0.6rem !important;
|
||||
padding-right: 2rem !important;
|
||||
.widget {
|
||||
padding-right: 1rem !important;
|
||||
padding-top: 0.65rem;
|
||||
padding-bottom: 0.6rem;
|
||||
padding-right: 2rem;
|
||||
.widget &.widget {
|
||||
padding-right: 1rem;
|
||||
@media (max-width: 510px) {
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
<div *ngIf="(federationAddresses$ | async) as federationAddresses; 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">{{ federationAddresses.length }} <span i18n="shared.addresses">addresses</span></div>
|
||||
<span class="fiat" *ngIf="(federationAddressesOneMonthAgo$ | async) as federationAddressesOneMonthAgo; else loadingSkeleton" i18n-ngbTooltip="liquid.percentage-change-last-month" ngbTooltip="Percentage change past month" placement="bottom">
|
||||
<app-change [current]="federationAddresses.length" [previous]="federationAddressesOneMonthAgo.addresses_count_one_month"></app-change>
|
||||
</span>
|
||||
</div>
|
||||
<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>
|
||||
@@ -30,5 +27,5 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loadingSkeleton>
|
||||
<div class="skeleton-loader skeleton-loader-transactions" style="margin-top: 2px; margin-bottom: 5px;"></div>
|
||||
<div class="skeleton-loader skeleton-loader-transactions" style="margin-top: 8px; margin-bottom: 8px;"></div>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.fee-estimation-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
@media (min-width: 376px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -21,6 +22,7 @@
|
||||
}
|
||||
|
||||
.card-text {
|
||||
padding-top: 9px;
|
||||
font-size: 22px;
|
||||
span {
|
||||
font-size: 11px;
|
||||
@@ -48,10 +50,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container{
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { FederationAddress } from '../../../interfaces/node-api.interface';
|
||||
import { Observable, combineLatest, map, of } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-federation-addresses-stats',
|
||||
@@ -9,12 +8,24 @@ import { FederationAddress } from '../../../interfaces/node-api.interface';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FederationAddressesStatsComponent implements OnInit {
|
||||
@Input() federationAddresses$: Observable<FederationAddress[]>;
|
||||
@Input() federationAddressesOneMonthAgo$: Observable<any>;
|
||||
@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}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,135 +1,133 @@
|
||||
<div class="container-xl">
|
||||
<div [ngClass]="{'widget': widget}">
|
||||
<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="recentPegs$ | 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 | slice:(page - 1) * pageSize:page * pageSize">
|
||||
<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: 140px"></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: 140px"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</table>
|
||||
|
||||
<ngb-pagination *ngIf="!widget && recentPegs$ | async as pegs" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
|
||||
[collectionSize]="pegs.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 *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>
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
|
||||
tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.65rem !important;
|
||||
padding-bottom: 0.6rem !important;
|
||||
padding-right: 2rem !important;
|
||||
.widget {
|
||||
padding-right: 1rem !important;
|
||||
padding-top: 0.65rem;
|
||||
padding-bottom: 0.6rem;
|
||||
padding-right: 2rem;
|
||||
.widget &.widget {
|
||||
padding-right: 1rem;
|
||||
@media (max-width: 510px) {
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +31,7 @@ tr, td, th {
|
||||
}
|
||||
|
||||
.transaction {
|
||||
width: 20%;
|
||||
width: 65%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -54,7 +57,7 @@ tr, td, th {
|
||||
}
|
||||
|
||||
.output {
|
||||
width: 20%;
|
||||
width: 50%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -93,7 +96,7 @@ tr, td, th {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
@media (max-width: 510px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { Observable, Subject, combineLatest, of, timer } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, Subject, combineLatest, of, timer } from 'rxjs';
|
||||
import { delayWhen, filter, map, share, shareReplay, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators';
|
||||
import { ApiService } from '../../../services/api.service';
|
||||
import { Env, StateService } from '../../../services/state.service';
|
||||
import { AuditStatus, CurrentPegs, FederationUtxo, RecentPeg } from '../../../interfaces/node-api.interface';
|
||||
import { AuditStatus, CurrentPegs, RecentPeg } from '../../../interfaces/node-api.interface';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
|
||||
@@ -15,21 +15,22 @@ import { SeoService } from '../../../services/seo.service';
|
||||
})
|
||||
export class RecentPegsListComponent implements OnInit {
|
||||
@Input() widget: boolean = false;
|
||||
@Input() recentPegIns$: Observable<RecentPeg[]> = of([]);
|
||||
@Input() recentPegOuts$: Observable<RecentPeg[]> = of([]);
|
||||
@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>;
|
||||
federationUtxos$: Observable<FederationUtxo[]>;
|
||||
recentPegs$: Observable<RecentPeg[]>;
|
||||
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;
|
||||
@@ -93,53 +94,36 @@ export class RecentPegsListComponent implements OnInit {
|
||||
share()
|
||||
);
|
||||
|
||||
this.federationUtxos$ = this.auditUpdated$.pipe(
|
||||
this.pegsCount$ = this.auditUpdated$.pipe(
|
||||
filter(auditUpdated => auditUpdated === true),
|
||||
throttleTime(40000),
|
||||
switchMap(_ => this.apiService.federationUtxos$()),
|
||||
tap(() => this.isPegCountLoading = true),
|
||||
switchMap(_ => this.apiService.pegsCount$()),
|
||||
map((data) => data.pegs_count),
|
||||
tap(() => this.isPegCountLoading = false),
|
||||
share()
|
||||
);
|
||||
|
||||
this.recentPegIns$ = this.federationUtxos$.pipe(
|
||||
map(federationUtxos => federationUtxos.filter(utxo => utxo.pegtxid).map(utxo => {
|
||||
return {
|
||||
txid: utxo.pegtxid,
|
||||
txindex: utxo.pegindex,
|
||||
amount: utxo.amount,
|
||||
bitcoinaddress: utxo.bitcoinaddress,
|
||||
bitcointxid: utxo.txid,
|
||||
bitcoinindex: utxo.txindex,
|
||||
blocktime: utxo.pegblocktime,
|
||||
}
|
||||
})),
|
||||
share()
|
||||
);
|
||||
|
||||
this.recentPegOuts$ = this.auditUpdated$.pipe(
|
||||
filter(auditUpdated => auditUpdated === true),
|
||||
throttleTime(40000),
|
||||
switchMap(_ => this.apiService.recentPegOuts$()),
|
||||
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()
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
this.recentPegs$ = combineLatest([
|
||||
this.recentPegIns$,
|
||||
this.recentPegOuts$
|
||||
]).pipe(
|
||||
map(([recentPegIns, recentPegOuts]) => {
|
||||
return [
|
||||
...recentPegIns,
|
||||
...recentPegOuts
|
||||
].sort((a, b) => {
|
||||
return b.blocktime - a.blocktime;
|
||||
});
|
||||
}),
|
||||
filter(recentPegs => recentPegs.length > 0),
|
||||
tap(_ => this.isLoading = false),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -148,6 +132,7 @@ export class RecentPegsListComponent implements OnInit {
|
||||
}
|
||||
|
||||
pageChange(page: number): void {
|
||||
this.startingIndexSubject.next((page - 1) * 15);
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.fee-estimation-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
@media (min-width: 376px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
<div class="container-xl dashboard-container" *ngIf="(auditStatus$ | async)?.isAuditSynced; else auditInProgress">
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2">
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<app-reserves-supply-stats [currentPeg$]="currentPeg$" [currentReserves$]="currentReserves$"></app-reserves-supply-stats>
|
||||
<app-reserves-ratio [currentPeg]="currentPeg$ | async" [currentReserves]="currentReserves$ | async"></app-reserves-ratio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
<app-reserves-ratio-stats [fullHistory$]="fullHistory$"></app-reserves-ratio-stats>
|
||||
</div>
|
||||
<div class="card-body pl-0" style="padding-top: 10px;">
|
||||
<app-reserves-ratio-graph [data]="fullHistory$ | async"></app-reserves-ratio-graph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<app-recent-pegs-stats [pegsVolume$]="pegsVolume$"></app-recent-pegs-stats>
|
||||
<app-recent-pegs-list [recentPegIns$]="recentPegIns$" [recentPegOuts$]="recentPegOuts$"[widget]="true"></app-recent-pegs-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<app-federation-addresses-stats [federationAddresses$]="federationAddresses$" [federationAddressesOneMonthAgo$]="federationAddressesOneMonthAgo$"></app-federation-addresses-stats>
|
||||
<app-federation-addresses-list [federationAddresses$]="federationAddresses$" [widget]="true"></app-federation-addresses-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<ng-template #loadingSkeleton>
|
||||
<div class="container-xl dashboard-container">
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2">
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<app-reserves-supply-stats></app-reserves-supply-stats>
|
||||
<app-reserves-ratio></app-reserves-ratio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
<app-reserves-ratio-stats></app-reserves-ratio-stats>
|
||||
</div>
|
||||
<div class="card-body pl-0" style="padding-top: 10px;">
|
||||
<app-reserves-ratio-graph></app-reserves-ratio-graph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<app-recent-pegs-stats></app-recent-pegs-stats>
|
||||
<app-recent-pegs-list [widget]="true"></app-recent-pegs-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<app-federation-addresses-stats></app-federation-addresses-stats>
|
||||
<app-federation-addresses-list [widget]="true"></app-federation-addresses-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #auditInProgress>
|
||||
<ng-container *ngIf="(auditStatus$ | async) as auditStatus; else loadingSkeleton">
|
||||
<div class="in-progress-message" *ngIf="auditStatus.lastBlockAudit && auditStatus.bitcoinHeaders; else loadingSkeleton">
|
||||
<span i18n="liquid.audit-in-progress">Audit in progress: Bitcoin block height #{{ auditStatus.lastBlockAudit }} / #{{ auditStatus.bitcoinHeaders }}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
@@ -1,138 +0,0 @@
|
||||
.dashboard-container {
|
||||
text-align: center;
|
||||
margin-top: 0.5rem;
|
||||
.col {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #1d1f31;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.card-body.pool-ranking {
|
||||
padding: 1.25rem 0.25rem 0.75rem 0.25rem;
|
||||
}
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
#blockchain-container {
|
||||
position: relative;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
#blockchain-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fade-border {
|
||||
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 80%, transparent 100%)
|
||||
}
|
||||
|
||||
.in-progress-message {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.more-padding {
|
||||
padding: 24px 20px !important;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
.card {
|
||||
height: auto !important;
|
||||
}
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex: inherit;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
padding: 22px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
&:first-child {
|
||||
max-width: 90px;
|
||||
margin: 15px auto 3px;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 10px auto 3px;
|
||||
max-width: 55px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.lastest-blocks-table {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.65rem !important;
|
||||
padding-bottom: 0.8rem !important;
|
||||
}
|
||||
.table-cell-height {
|
||||
width: 25%;
|
||||
}
|
||||
.table-cell-fee {
|
||||
width: 25%;
|
||||
text-align: right;
|
||||
}
|
||||
.table-cell-pool {
|
||||
text-align: left;
|
||||
width: 30%;
|
||||
|
||||
@media (max-width: 875px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pool-name {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
.table-cell-acceleration-count {
|
||||
text-align: right;
|
||||
width: 20%;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
height: 385px;
|
||||
}
|
||||
.list-card {
|
||||
height: 410px;
|
||||
@media (max-width: 767px) {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.mempool-block-wrapper {
|
||||
max-height: 380px;
|
||||
max-width: 380px;
|
||||
margin: auto;
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { Observable, Subject, combineLatest, delayWhen, filter, interval, map, of, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime, timer } from 'rxjs';
|
||||
import { ApiService } from '../../../services/api.service';
|
||||
import { AuditStatus, CurrentPegs, FederationAddress, FederationUtxo, PegsVolume, RecentPeg } from '../../../interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-reserves-audit-dashboard',
|
||||
templateUrl: './reserves-audit-dashboard.component.html',
|
||||
styleUrls: ['./reserves-audit-dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ReservesAuditDashboardComponent implements OnInit {
|
||||
auditStatus$: Observable<AuditStatus>;
|
||||
auditUpdated$: Observable<boolean>;
|
||||
currentPeg$: Observable<CurrentPegs>;
|
||||
currentReserves$: Observable<CurrentPegs>;
|
||||
federationUtxos$: Observable<FederationUtxo[]>;
|
||||
recentPegIns$: Observable<RecentPeg[]>;
|
||||
recentPegOuts$: Observable<RecentPeg[]>;
|
||||
pegsVolume$: Observable<PegsVolume[]>;
|
||||
federationAddresses$: Observable<FederationAddress[]>;
|
||||
federationAddressesOneMonthAgo$: Observable<any>;
|
||||
liquidPegsMonth$: Observable<any>;
|
||||
liquidReservesMonth$: Observable<any>;
|
||||
fullHistory$: Observable<any>;
|
||||
isLoad: boolean = true;
|
||||
private lastPegBlockUpdate: number = 0;
|
||||
private lastPegAmount: string = '';
|
||||
private lastReservesBlockUpdate: number = 0;
|
||||
|
||||
private destroy$ = new Subject();
|
||||
|
||||
constructor(
|
||||
private seoService: SeoService,
|
||||
private websocketService: WebsocketService,
|
||||
private apiService: ApiService,
|
||||
private stateService: StateService,
|
||||
) {
|
||||
this.seoService.setTitle($localize`:@@liquid.reserves-audit:Reserves Audit Dashboard`);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks', 'mempool-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.lastPegAmount = currentPegAmount;
|
||||
return of(blockAuditCheck || amountCheck);
|
||||
}),
|
||||
share()
|
||||
);
|
||||
|
||||
this.currentReserves$ = this.auditUpdated$.pipe(
|
||||
filter(auditUpdated => auditUpdated === true),
|
||||
throttleTime(40000),
|
||||
switchMap(_ =>
|
||||
this.apiService.liquidReserves$().pipe(
|
||||
filter((currentReserves) => currentReserves.lastBlockUpdate >= this.lastReservesBlockUpdate),
|
||||
tap((currentReserves) => {
|
||||
this.lastReservesBlockUpdate = currentReserves.lastBlockUpdate;
|
||||
})
|
||||
)
|
||||
),
|
||||
share()
|
||||
);
|
||||
|
||||
this.federationUtxos$ = this.auditUpdated$.pipe(
|
||||
filter(auditUpdated => auditUpdated === true),
|
||||
throttleTime(40000),
|
||||
switchMap(_ => this.apiService.federationUtxos$()),
|
||||
share()
|
||||
);
|
||||
|
||||
this.recentPegIns$ = this.federationUtxos$.pipe(
|
||||
map(federationUtxos => federationUtxos.filter(utxo => utxo.pegtxid).map(utxo => {
|
||||
return {
|
||||
txid: utxo.pegtxid,
|
||||
txindex: utxo.pegindex,
|
||||
amount: utxo.amount,
|
||||
bitcoinaddress: utxo.bitcoinaddress,
|
||||
bitcointxid: utxo.txid,
|
||||
bitcoinindex: utxo.txindex,
|
||||
blocktime: utxo.pegblocktime,
|
||||
}
|
||||
})),
|
||||
share()
|
||||
);
|
||||
|
||||
this.recentPegOuts$ = this.auditUpdated$.pipe(
|
||||
filter(auditUpdated => auditUpdated === true),
|
||||
throttleTime(40000),
|
||||
switchMap(_ => this.apiService.recentPegOuts$()),
|
||||
share()
|
||||
);
|
||||
|
||||
this.pegsVolume$ = this.auditUpdated$.pipe(
|
||||
filter(auditUpdated => auditUpdated === true),
|
||||
throttleTime(40000),
|
||||
switchMap(_ => this.apiService.pegsVolume$()),
|
||||
share()
|
||||
);
|
||||
|
||||
this.federationAddresses$ = this.auditUpdated$.pipe(
|
||||
filter(auditUpdated => auditUpdated === true),
|
||||
throttleTime(40000),
|
||||
switchMap(_ => this.apiService.federationAddresses$()),
|
||||
share()
|
||||
);
|
||||
|
||||
this.federationAddressesOneMonthAgo$ = interval(60 * 60 * 1000)
|
||||
.pipe(
|
||||
startWith(0),
|
||||
switchMap(() => this.apiService.federationAddressesOneMonthAgo$())
|
||||
);
|
||||
|
||||
this.liquidPegsMonth$ = interval(60 * 60 * 1000)
|
||||
.pipe(
|
||||
startWith(0),
|
||||
switchMap(() => this.apiService.listLiquidPegsMonth$()),
|
||||
map((pegs) => {
|
||||
const labels = pegs.map(stats => stats.date);
|
||||
const series = pegs.map(stats => parseFloat(stats.amount) / 100000000);
|
||||
series.reduce((prev, curr, i) => series[i] = prev + curr, 0);
|
||||
return {
|
||||
series,
|
||||
labels
|
||||
};
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
|
||||
this.liquidReservesMonth$ = interval(60 * 60 * 1000).pipe(
|
||||
startWith(0),
|
||||
switchMap(() => this.apiService.listLiquidReservesMonth$()),
|
||||
map(reserves => {
|
||||
const labels = reserves.map(stats => stats.date);
|
||||
const series = reserves.map(stats => parseFloat(stats.amount) / 100000000);
|
||||
return {
|
||||
series,
|
||||
labels
|
||||
};
|
||||
}),
|
||||
share()
|
||||
);
|
||||
|
||||
this.fullHistory$ = combineLatest([this.liquidPegsMonth$, this.currentPeg$, this.liquidReservesMonth$, this.currentReserves$])
|
||||
.pipe(
|
||||
map(([liquidPegs, currentPeg, liquidReserves, currentReserves]) => {
|
||||
liquidPegs.series[liquidPegs.series.length - 1] = parseFloat(currentPeg.amount) / 100000000;
|
||||
|
||||
if (liquidPegs.series.length === liquidReserves?.series.length) {
|
||||
liquidReserves.series[liquidReserves.series.length - 1] = parseFloat(currentReserves?.amount) / 100000000;
|
||||
} else if (liquidPegs.series.length === liquidReserves?.series.length + 1) {
|
||||
liquidReserves.series.push(parseFloat(currentReserves?.amount) / 100000000);
|
||||
liquidReserves.labels.push(liquidPegs.labels[liquidPegs.labels.length - 1]);
|
||||
} else {
|
||||
liquidReserves = {
|
||||
series: [],
|
||||
labels: []
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
liquidPegs,
|
||||
liquidReserves
|
||||
};
|
||||
}),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next(1);
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
<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.toFixed(5) }}
|
||||
{{ (unbackedMonths.avg * 100).toFixed(3) }} %
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<div class="echarts" echarts [initOpts]="ratioHistoryChartInitOptions" [options]="ratioHistoryChartOptions" (chartRendered)="rendered()"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
@@ -1,6 +0,0 @@
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 16px);
|
||||
z-index: 100;
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
import { Component, Inject, LOCALE_ID, ChangeDetectionStrategy, Input, OnChanges, OnInit } from '@angular/core';
|
||||
import { formatDate, formatNumber } from '@angular/common';
|
||||
import { EChartsOption } from '../../../graphs/echarts';
|
||||
|
||||
@Component({
|
||||
selector: 'app-reserves-ratio-graph',
|
||||
templateUrl: './reserves-ratio-graph.component.html',
|
||||
styleUrls: ['./reserves-ratio-graph.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ReservesRatioGraphComponent implements OnInit, OnChanges {
|
||||
@Input() data: any;
|
||||
ratioHistoryChartOptions: EChartsOption;
|
||||
ratioSeries: number[] = [];
|
||||
|
||||
height: number | string = '200';
|
||||
right: number | string = '10';
|
||||
top: number | string = '20';
|
||||
left: number | string = '50';
|
||||
template: ('widget' | 'advanced') = 'widget';
|
||||
isLoading = true;
|
||||
|
||||
ratioHistoryChartInitOptions = {
|
||||
renderer: 'svg'
|
||||
};
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.isLoading = true;
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
if (!this.data) {
|
||||
return;
|
||||
}
|
||||
// Compute the ratio series: the ratio of the reserves to the pegs
|
||||
this.ratioSeries = this.data.liquidReserves.series.map((value: number, index: number) => value / this.data.liquidPegs.series[index]);
|
||||
// Truncate the ratio series and labels series to last 3 years
|
||||
this.ratioSeries = this.ratioSeries.slice(Math.max(this.ratioSeries.length - 36, 0));
|
||||
this.data.liquidPegs.labels = this.data.liquidPegs.labels.slice(Math.max(this.data.liquidPegs.labels.length - 36, 0));
|
||||
// Cut the values that are too high or too low
|
||||
this.ratioSeries = this.ratioSeries.map((value: number) => Math.min(Math.max(value, 0.995), 1.005));
|
||||
this.ratioHistoryChartOptions = this.createChartOptions(this.ratioSeries, this.data.liquidPegs.labels);
|
||||
}
|
||||
|
||||
rendered() {
|
||||
if (!this.data) {
|
||||
return;
|
||||
}
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
createChartOptions(ratioSeries: number[], labels: string[]): EChartsOption {
|
||||
return {
|
||||
grid: {
|
||||
height: this.height,
|
||||
right: this.right,
|
||||
top: this.top,
|
||||
left: this.left,
|
||||
},
|
||||
animation: false,
|
||||
dataZoom: [{
|
||||
type: 'inside',
|
||||
realtime: true,
|
||||
zoomOnMouseWheel: (this.template === 'advanced') ? true : false,
|
||||
maxSpan: 100,
|
||||
minSpan: 10,
|
||||
}, {
|
||||
show: (this.template === 'advanced') ? true : false,
|
||||
type: 'slider',
|
||||
brushSelect: false,
|
||||
realtime: true,
|
||||
selectedDataBackground: {
|
||||
lineStyle: {
|
||||
color: '#fff',
|
||||
opacity: 0.45,
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0,
|
||||
}
|
||||
}
|
||||
}],
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
position: (pos, params, el, elRect, size) => {
|
||||
const obj = { top: -20 };
|
||||
obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80;
|
||||
return obj;
|
||||
},
|
||||
extraCssText: `width: ${(this.template === 'widget') ? '125px' : '135px'};
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;`,
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
const colorSpan = (color: string) => `<span class="indicator" style="background-color: ${color};"></span>`;
|
||||
let itemFormatted = '<div class="title">' + params[0].axisValue + '</div>';
|
||||
const item = params[0];
|
||||
const formattedValue = formatNumber(item.value, this.locale, '1.5-5');
|
||||
const symbol = (item.value === 1.005) ? '≥ ' : (item.value === 0.995) ? '≤ ' : '';
|
||||
itemFormatted += `<div class="item">
|
||||
<div class="indicator-container">${colorSpan(item.color)}</div>
|
||||
<div style="margin-right: 5px"></div>
|
||||
<div class="value">${symbol}${formattedValue}</div>
|
||||
</div>`;
|
||||
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}">${itemFormatted}</div>`;
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
axisLabel: {
|
||||
align: 'center',
|
||||
fontSize: 11,
|
||||
lineHeight: 12
|
||||
},
|
||||
boundaryGap: false,
|
||||
data: labels.map((value: any) => `${formatDate(value, 'MMM\ny', this.locale)}`),
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dotted',
|
||||
color: '#ffffff66',
|
||||
opacity: 0.25,
|
||||
}
|
||||
},
|
||||
min: 0.995,
|
||||
max: 1.005,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: ratioSeries,
|
||||
name: '',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
|
||||
},
|
||||
markLine: {
|
||||
silent: true,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
color: '#fff',
|
||||
opacity: 1,
|
||||
width: 1,
|
||||
},
|
||||
data: [{
|
||||
yAxis: 1,
|
||||
label: {
|
||||
show: false,
|
||||
color: '#ffffff',
|
||||
}
|
||||
}],
|
||||
},
|
||||
},
|
||||
],
|
||||
visualMap: {
|
||||
show: false,
|
||||
top: 50,
|
||||
right: 10,
|
||||
pieces: [{
|
||||
gt: 0,
|
||||
lte: 0.999,
|
||||
color: '#D81B60'
|
||||
},
|
||||
{
|
||||
gt: 0.999,
|
||||
lte: 1.001,
|
||||
color: '#FDD835'
|
||||
},
|
||||
{
|
||||
gt: 1.001,
|
||||
lte: 2,
|
||||
color: '#7CB342'
|
||||
}
|
||||
],
|
||||
outOfRange: {
|
||||
color: '#999'
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, OnChanges, OnInit } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, Input, OnChanges, OnInit, HostListener } from '@angular/core';
|
||||
import { EChartsOption } from '../../../graphs/echarts';
|
||||
import { CurrentPegs } from '../../../interfaces/node-api.interface';
|
||||
|
||||
@@ -32,6 +32,10 @@ export class ReservesRatioComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.updateChartOptions();
|
||||
}
|
||||
|
||||
updateChartOptions() {
|
||||
if (!this.currentPeg || !this.currentReserves || this.currentPeg.amount === '0') {
|
||||
return;
|
||||
}
|
||||
@@ -46,13 +50,43 @@ export class ReservesRatioComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
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%', '70%'],
|
||||
center: ['50%', '75%'],
|
||||
radius: '100%',
|
||||
min: 0.999,
|
||||
max: 1.001,
|
||||
@@ -68,13 +102,23 @@ export class ReservesRatioComponent implements OnInit, OnChanges {
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'inherit',
|
||||
fontFamily: '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: '50%',
|
||||
width: 16,
|
||||
offsetCenter: [0, '-27%'],
|
||||
length: pointerLength,
|
||||
width: pointerWidth,
|
||||
offsetCenter: offsetCenter,
|
||||
itemStyle: {
|
||||
color: 'auto'
|
||||
}
|
||||
@@ -95,7 +139,7 @@ export class ReservesRatioComponent implements OnInit, OnChanges {
|
||||
},
|
||||
title: {
|
||||
show: true,
|
||||
offsetCenter: [0, '-117.5%'],
|
||||
offsetCenter: [0, '-127%'],
|
||||
fontSize: 18,
|
||||
color: '#4a68b9',
|
||||
fontFamily: 'inherit',
|
||||
@@ -108,19 +152,24 @@ export class ReservesRatioComponent implements OnInit, OnChanges {
|
||||
fontFamily: 'inherit',
|
||||
fontWeight: 500,
|
||||
formatter: function (value) {
|
||||
return (value).toFixed(5);
|
||||
return (value * 100).toFixed(3) + '%';
|
||||
},
|
||||
color: 'inherit'
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: parseFloat(currentReserves.amount) / parseFloat(currentPeg.amount),
|
||||
name: 'Peg-O-Meter'
|
||||
value: value,
|
||||
name: 'Assets vs Liabilities'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
this.updateChartOptions();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,29 @@
|
||||
<div *ngIf="(currentPeg$ | async) as currentPeg; else loadingData">
|
||||
<div *ngIf="(currentReserves$ | async) as currentReserves; else loadingData">
|
||||
<div class="fee-estimation-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5>
|
||||
<div class="card-text">
|
||||
<div class="fee-text">{{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} <span>L-BTC</span></div>
|
||||
<span class="fiat">
|
||||
<span>As of block <a [routerLink]="['/block', currentPeg.hash]">{{ currentPeg.lastBlockUpdate }}</a></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.btc-reserves">BTC Reserves</h5>
|
||||
<div 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>As of block <a href="{{ env.MEMPOOL_WEBSITE_URL + '/block/' + currentReserves.hash }}" target="_blank" style="color:#b86d12">{{ currentReserves.lastBlockUpdate }}</a></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<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="fee-estimation-container loading-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</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="dashboard.btc-reserves">BTC Reserves</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
}
|
||||
.fee-text{
|
||||
border-bottom: 1px solid #ffffff1c;
|
||||
color: #ffffff;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
line-height: 1.45;
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<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">
|
||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-home" *ngIf="network.val === '' && 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -163,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>` +
|
||||
@@ -201,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>` +
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
</ng-template>
|
||||
<ng-template #timeEstimateDefault>
|
||||
<span class="eta justify-content-end" [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'd-flex align-items-center' : ''">
|
||||
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.timeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.adjustedTimeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
<a *ngIf="!tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerate btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
||||
</span>
|
||||
</ng-template>
|
||||
|
||||
@@ -26,6 +26,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
|
||||
import { Price, PriceService } from '../../services/price.service';
|
||||
import { isFeatureActive } from '../../bitcoin.utils';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transaction',
|
||||
@@ -116,12 +117,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
private servicesApiService: ServicesApiServices,
|
||||
private seoService: SeoService,
|
||||
private priceService: PriceService,
|
||||
private storageService: StorageService
|
||||
private storageService: StorageService,
|
||||
private enterpriseService: EnterpriseService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
|
||||
|
||||
this.enterpriseService.page();
|
||||
|
||||
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||
this.stateService.networkChanged$.subscribe(
|
||||
(network) => {
|
||||
@@ -527,6 +531,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (!this.txId) {
|
||||
return;
|
||||
}
|
||||
this.enterpriseService.goal(8);
|
||||
this.showAccelerationSummary = true && this.acceleratorAvailable;
|
||||
this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview;
|
||||
return false;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
<div class="container-xl dashboard-container">
|
||||
<div class="container-xl dashboard-container" *ngIf="(network$ | async) !== 'liquid'; else liquidDashboard">
|
||||
<div class="row row-cols-1 row-cols-md-2" *ngIf="{ value: (mempoolInfoData$ | async) } as mempoolInfoData">
|
||||
<ng-container *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'">
|
||||
<ng-container *ngIf="(network$ | async) !== 'liquidtestnet'">
|
||||
<div class="col card-wrapper">
|
||||
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
|
||||
<div class="card">
|
||||
@@ -17,35 +17,26 @@
|
||||
<div class="col">
|
||||
<div class="card graph-card">
|
||||
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
|
||||
<ng-template [ngIf]="(network$ | async) !== 'liquid'" [ngIfElse]="liquidPegs">
|
||||
<a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/mempool-block/0' | relativeUrl]">
|
||||
<h5 class="card-title d-inline"><span i18n="dashboard.mempool-goggles">Mempool Goggles</span>: {{ goggleCycle[goggleIndex].name }}</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="quick-filter">
|
||||
<div class="btn-group btn-group-toggle">
|
||||
<label class="btn btn-primary btn-xs" [class.active]="filter.index === goggleIndex" *ngFor="let filter of goggleCycle">
|
||||
<input type="radio" [value]="'3m'" fragment="3m" (click)="setFilter(filter.index)" [attr.data-cy]="'3m'"> {{ filter.name }}
|
||||
</label>
|
||||
</div>
|
||||
<a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/mempool-block/0' | relativeUrl]">
|
||||
<h5 class="card-title d-inline"><span i18n="dashboard.mempool-goggles">Mempool Goggles</span>: {{ goggleCycle[goggleIndex].name }}</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="quick-filter">
|
||||
<div class="btn-group btn-group-toggle">
|
||||
<label class="btn btn-primary btn-xs" [class.active]="filter.index === goggleIndex" *ngFor="let filter of goggleCycle">
|
||||
<input type="radio" [value]="'3m'" fragment="3m" (click)="setFilter(filter.index)" [attr.data-cy]="'3m'"> {{ filter.name }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="mempool-block-wrapper">
|
||||
<app-mempool-block-overview
|
||||
[index]="0"
|
||||
[resolution]="goggleResolution"
|
||||
[filterFlags]="goggleFlags"
|
||||
[filterMode]="goggleMode"
|
||||
></app-mempool-block-overview>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template #liquidPegs>
|
||||
<div style="padding-left: 1.25rem;">
|
||||
<ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
|
||||
<hr>
|
||||
</div>
|
||||
<app-lbtc-pegs-graph [data]="fullHistory$ | async" [height]="lbtcPegGraphHeight"></app-lbtc-pegs-graph>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="mempool-block-wrapper">
|
||||
<app-mempool-block-overview
|
||||
[index]="0"
|
||||
[resolution]="goggleResolution"
|
||||
[filterFlags]="goggleFlags"
|
||||
[filterMode]="goggleMode"
|
||||
></app-mempool-block-overview>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,7 +44,6 @@
|
||||
<div class="card graph-card">
|
||||
<div class="card-body">
|
||||
<ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
|
||||
<ng-container *ngIf="stateService.network !== 'liquid'">
|
||||
<h5 class="card-title mt-3" i18n="dashboard.incoming-transactions">Incoming Transactions</h5>
|
||||
<div class="mempool-graph" *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
|
||||
<app-incoming-transactions-graph
|
||||
@@ -64,30 +54,11 @@
|
||||
[windowPreferenceOverride]="'2h'"
|
||||
></app-incoming-transactions-graph>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="mempool-graph" *ngIf="stateService.network === 'liquid'">
|
||||
<hr>
|
||||
<table class="table table-borderless table-striped" *ngIf="(featuredAssets$ | async) as featuredAssets else loadingAssetsTable">
|
||||
<tbody>
|
||||
<tr *ngFor="let group of featuredAssets">
|
||||
<td class="asset-icon">
|
||||
<a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">
|
||||
<img class="assetIcon" [src]="'/api/v1/asset/' + group.asset + '/icon'">
|
||||
</a>
|
||||
</td>
|
||||
<td class="asset-title">
|
||||
<a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">{{ group.name }}</a>
|
||||
</td>
|
||||
<td class="circulating-amount"><app-asset-circulation [assetId]="group.asset"></app-asset-circulation></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col" style="max-height: 410px">
|
||||
<div class="card" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'; else latestBlocks">
|
||||
<div class="card" *ngIf="(network$ | async) !== 'liquidtestnet'; else latestBlocks">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.recent-rbf-replacements">Recent Replacements</h5>
|
||||
@@ -171,7 +142,7 @@
|
||||
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<td class="table-cell-satoshis"><app-amount *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'; else liquidAmount" [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount><ng-template #liquidAmount i18n="shared.confidential">Confidential</ng-template></td>
|
||||
<td class="table-cell-satoshis"><app-amount *ngIf="(network$ | async) !== 'liquidtestnet'; else liquidAmount" [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount><ng-template #liquidAmount i18n="shared.confidential">Confidential</ng-template></td>
|
||||
<td class="table-cell-fiat" *ngIf="(network$ | async) === ''" ><app-fiat [value]="transaction.value" digitsInfo="1.0-0"></app-fiat></td>
|
||||
<td class="table-cell-fees"><app-fee-rate [fee]="transaction.fee" [weight]="transaction.vsize * 4"></app-fee-rate></td>
|
||||
</tr>
|
||||
@@ -184,25 +155,50 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingAssetsTable>
|
||||
<table class="table table-borderless table-striped asset-table">
|
||||
<tbody>
|
||||
<tr *ngFor="let i of getArrayFromNumber(this.nbFeaturedAssets)">
|
||||
<td class="asset-icon">
|
||||
<div class="skeleton-loader skeleton-loader-transactions"></div>
|
||||
</td>
|
||||
<td class="asset-title">
|
||||
<div class="skeleton-loader skeleton-loader-transactions"></div>
|
||||
</td>
|
||||
<td class="asset-title d-none d-md-table-cell">
|
||||
<div class="skeleton-loader skeleton-loader-transactions"></div>
|
||||
</td>
|
||||
<td class="asset-title">
|
||||
<div class="skeleton-loader skeleton-loader-transactions"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<ng-template #liquidDashboard>
|
||||
<div class="container-xl dashboard-container" *ngIf="(auditStatus$ | async)?.isAuditSynced; else auditInProgress">
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2">
|
||||
|
||||
<div class="col">
|
||||
<div class="card-liquid card">
|
||||
<div class="card-title card-title-liquid">
|
||||
<app-reserves-supply-stats [currentPeg$]="currentPeg$" [currentReserves$]="currentReserves$"></app-reserves-supply-stats>
|
||||
</div>
|
||||
<div class="card-body pl-0" style="padding-top: 10px;">
|
||||
<app-lbtc-pegs-graph [data]="fullHistory$ | async" [height]="lbtcPegGraphHeight"></app-lbtc-pegs-graph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card-liquid card">
|
||||
<div class="card-body">
|
||||
<app-reserves-ratio-stats [fullHistory$]="fullHistory$"></app-reserves-ratio-stats>
|
||||
<app-reserves-ratio [currentPeg]="currentPeg$ | async" [currentReserves]="currentReserves$ | async"></app-reserves-ratio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card card-liquid smaller">
|
||||
<div class="card-body">
|
||||
<app-recent-pegs-stats [pegsVolume$]="pegsVolume$"></app-recent-pegs-stats>
|
||||
<app-recent-pegs-list [recentPegsList$]="recentPegsList$" [widget]="true"></app-recent-pegs-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card-liquid card smaller">
|
||||
<div class="card-body">
|
||||
<app-federation-addresses-stats [federationAddressesNumber$]="federationAddressesNumber$" [federationUtxosNumber$]="federationUtxosNumber$"></app-federation-addresses-stats>
|
||||
<app-federation-addresses-list [federationAddresses$]="federationAddresses$" [widget]="true"></app-federation-addresses-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #replacementsSkeleton>
|
||||
@@ -283,21 +279,56 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #lbtcPegs let-mempoolInfoData>
|
||||
<div class="mempool-info-data">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5>
|
||||
<ng-container *ngIf="(currentPeg$ | async) as currentPeg; else loadingTransactions">
|
||||
<p i18n-ngbTooltip="liquid.last-elements-audit-block" [ngbTooltip]="'L-BTC supply last updated at Liquid block ' + (currentPeg.lastBlockUpdate)" placement="top" class="card-text">{{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} <span>L-BTC</span></p>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a class="title-link" [routerLink]="['/audit' | relativeUrl]">
|
||||
<h5 class="card-title"><ng-container i18n="dashboard.btc-reserves">BTC Reserves</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
|
||||
</a>
|
||||
<ng-container *ngIf="(currentReserves$ | async) as currentReserves; else loadingTransactions">
|
||||
<p i18n-ngbTooltip="liquid.last-bitcoin-audit-block" [ngbTooltip]="'BTC reserves last updated at Bitcoin block ' + (currentReserves.lastBlockUpdate)" placement="top" class="card-text">{{ +(currentReserves.amount) / 100000000 | number: '1.2-2' }} <span class="bitcoin-color">BTC</span></p>
|
||||
</ng-container>
|
||||
<ng-template #loadingSkeletonLiquid>
|
||||
<div class="container-xl dashboard-container">
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2">
|
||||
|
||||
<div class="col">
|
||||
<div class="card-liquid card">
|
||||
<div class="card-title card-title-liquid">
|
||||
<app-reserves-supply-stats></app-reserves-supply-stats>
|
||||
</div>
|
||||
<div class="card-body pl-0" style="padding-top: 10px;">
|
||||
<app-lbtc-pegs-graph [height]="lbtcPegGraphHeight"></app-lbtc-pegs-graph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card-liquid card">
|
||||
<div class="card-body">
|
||||
<app-reserves-ratio-stats></app-reserves-ratio-stats>
|
||||
<app-reserves-ratio></app-reserves-ratio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card card-liquid smaller">
|
||||
<div class="card-body">
|
||||
<app-recent-pegs-stats></app-recent-pegs-stats>
|
||||
<app-recent-pegs-list [widget]="true"></app-recent-pegs-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card-liquid card smaller">
|
||||
<div class="card-body">
|
||||
<app-federation-addresses-stats></app-federation-addresses-stats>
|
||||
<app-federation-addresses-list [widget]="true"></app-federation-addresses-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #auditInProgress>
|
||||
<ng-container *ngIf="(auditStatus$ | async) as auditStatus; else loadingSkeletonLiquid">
|
||||
<div class="in-progress-message" *ngIf="auditStatus.lastBlockAudit && auditStatus.bitcoinHeaders; else loadingSkeletonLiquid">
|
||||
<span i18n="liquid.audit-in-progress">Audit in progress: Bitcoin block height #{{ auditStatus.lastBlockAudit }} / #{{ auditStatus.bitcoinHeaders }}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
@@ -59,6 +59,10 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
&.lbtc-pegs-stats {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
h5 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@@ -409,3 +413,28 @@
|
||||
margin-top: 5px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.card-liquid {
|
||||
background-color: #1d1f31;
|
||||
height: 418px;
|
||||
@media (min-width: 992px) {
|
||||
height: 512px;
|
||||
}
|
||||
&.smaller {
|
||||
height: 408px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-title-liquid {
|
||||
padding-top: 20px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.in-progress-message {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, OnDestroy, OnInit } from '@angular/core';
|
||||
import { combineLatest, EMPTY, fromEvent, merge, Observable, of, Subject, Subscription, timer } from 'rxjs';
|
||||
import { combineLatest, EMPTY, fromEvent, interval, merge, Observable, of, Subject, Subscription, timer } from 'rxjs';
|
||||
import { catchError, delayWhen, distinctUntilChanged, filter, map, scan, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators';
|
||||
import { AuditStatus, BlockExtended, CurrentPegs, OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
||||
import { AuditStatus, BlockExtended, CurrentPegs, FederationAddress, OptimizedMempoolStats, PegsVolume, RecentPeg } from '../interfaces/node-api.interface';
|
||||
import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { StateService } from '../services/state.service';
|
||||
@@ -33,8 +33,6 @@ interface MempoolStatsData {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
featuredAssets$: Observable<any>;
|
||||
nbFeaturedAssets = 6;
|
||||
network$: Observable<string>;
|
||||
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
||||
mempoolInfoData$: Observable<MempoolInfoData>;
|
||||
@@ -54,6 +52,11 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
auditUpdated$: Observable<boolean>;
|
||||
liquidReservesMonth$: Observable<any>;
|
||||
currentReserves$: Observable<CurrentPegs>;
|
||||
recentPegsList$: Observable<RecentPeg[]>;
|
||||
pegsVolume$: Observable<PegsVolume[]>;
|
||||
federationAddresses$: Observable<FederationAddress[]>;
|
||||
federationAddressesNumber$: Observable<number>;
|
||||
federationUtxosNumber$: Observable<number>;
|
||||
fullHistory$: Observable<any>;
|
||||
isLoad: boolean = true;
|
||||
filterSubscription: Subscription;
|
||||
@@ -184,26 +187,6 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
})
|
||||
);
|
||||
|
||||
const windowResize$ = fromEvent(window, 'resize').pipe(
|
||||
distinctUntilChanged(),
|
||||
startWith(null)
|
||||
);
|
||||
|
||||
this.featuredAssets$ = combineLatest([
|
||||
this.apiService.listFeaturedAssets$(),
|
||||
windowResize$
|
||||
]).pipe(
|
||||
map(([featured, _]) => {
|
||||
const newArray = [];
|
||||
for (const feature of featured) {
|
||||
if (feature.ticker !== 'L-BTC' && feature.asset) {
|
||||
newArray.push(feature);
|
||||
}
|
||||
}
|
||||
return newArray.slice(0, this.nbFeaturedAssets);
|
||||
}),
|
||||
);
|
||||
|
||||
this.transactions$ = this.stateService.transactions$
|
||||
.pipe(
|
||||
scan((acc, tx) => {
|
||||
@@ -269,7 +252,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
share(),
|
||||
);
|
||||
|
||||
if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
|
||||
if (this.stateService.network === 'liquid') {
|
||||
this.auditStatus$ = this.stateService.blocks$.pipe(
|
||||
takeUntil(this.destroy$),
|
||||
throttleTime(40000),
|
||||
@@ -279,22 +262,6 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
////////// Pegs historical data //////////
|
||||
this.liquidPegsMonth$ = this.auditStatus$.pipe(
|
||||
throttleTime(60 * 60 * 1000),
|
||||
switchMap(() => this.apiService.listLiquidPegsMonth$()),
|
||||
map((pegs) => {
|
||||
const labels = pegs.map(stats => stats.date);
|
||||
const series = pegs.map(stats => parseFloat(stats.amount) / 100000000);
|
||||
series.reduce((prev, curr, i) => series[i] = prev + curr, 0);
|
||||
return {
|
||||
series,
|
||||
labels
|
||||
};
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
|
||||
this.currentPeg$ = this.auditStatus$.pipe(
|
||||
switchMap(_ =>
|
||||
this.apiService.liquidPegs$().pipe(
|
||||
@@ -307,7 +274,6 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
share()
|
||||
);
|
||||
|
||||
////////// BTC Reserves historical data //////////
|
||||
this.auditUpdated$ = combineLatest([
|
||||
this.auditStatus$,
|
||||
this.currentPeg$
|
||||
@@ -322,21 +288,6 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
const amountCheck = currentPegAmount !== this.lastPegAmount;
|
||||
this.lastPegAmount = currentPegAmount;
|
||||
return of(blockAuditCheck || amountCheck);
|
||||
})
|
||||
);
|
||||
|
||||
this.liquidReservesMonth$ = this.auditStatus$.pipe(
|
||||
throttleTime(60 * 60 * 1000),
|
||||
switchMap((auditStatus) => {
|
||||
return auditStatus.isAuditSynced ? this.apiService.listLiquidReservesMonth$() : EMPTY;
|
||||
}),
|
||||
map(reserves => {
|
||||
const labels = reserves.map(stats => stats.date);
|
||||
const series = reserves.map(stats => parseFloat(stats.amount) / 100000000);
|
||||
return {
|
||||
series,
|
||||
labels
|
||||
};
|
||||
}),
|
||||
share()
|
||||
);
|
||||
@@ -355,11 +306,78 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
share()
|
||||
);
|
||||
|
||||
this.fullHistory$ = combineLatest([this.liquidPegsMonth$, this.currentPeg$, this.liquidReservesMonth$.pipe(startWith(null)), this.currentReserves$.pipe(startWith(null))])
|
||||
this.recentPegsList$ = this.auditUpdated$.pipe(
|
||||
filter(auditUpdated => auditUpdated === true),
|
||||
throttleTime(40000),
|
||||
switchMap(_ => this.apiService.recentPegsList$()),
|
||||
share()
|
||||
);
|
||||
|
||||
this.pegsVolume$ = this.auditUpdated$.pipe(
|
||||
filter(auditUpdated => auditUpdated === true),
|
||||
throttleTime(40000),
|
||||
switchMap(_ => this.apiService.pegsVolume$()),
|
||||
share()
|
||||
);
|
||||
|
||||
this.federationAddresses$ = this.auditUpdated$.pipe(
|
||||
filter(auditUpdated => auditUpdated === true),
|
||||
throttleTime(40000),
|
||||
switchMap(_ => this.apiService.federationAddresses$()),
|
||||
share()
|
||||
);
|
||||
|
||||
this.federationAddressesNumber$ = this.auditUpdated$.pipe(
|
||||
filter(auditUpdated => auditUpdated === true),
|
||||
throttleTime(40000),
|
||||
switchMap(_ => this.apiService.federationAddressesNumber$()),
|
||||
map(count => count.address_count),
|
||||
share()
|
||||
);
|
||||
|
||||
this.federationUtxosNumber$ = this.auditUpdated$.pipe(
|
||||
filter(auditUpdated => auditUpdated === true),
|
||||
throttleTime(40000),
|
||||
switchMap(_ => this.apiService.federationUtxosNumber$()),
|
||||
map(count => count.utxo_count),
|
||||
share()
|
||||
);
|
||||
|
||||
this.liquidPegsMonth$ = interval(60 * 60 * 1000)
|
||||
.pipe(
|
||||
startWith(0),
|
||||
switchMap(() => this.apiService.listLiquidPegsMonth$()),
|
||||
map((pegs) => {
|
||||
const labels = pegs.map(stats => stats.date);
|
||||
const series = pegs.map(stats => parseFloat(stats.amount) / 100000000);
|
||||
series.reduce((prev, curr, i) => series[i] = prev + curr, 0);
|
||||
return {
|
||||
series,
|
||||
labels
|
||||
};
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
|
||||
this.liquidReservesMonth$ = interval(60 * 60 * 1000).pipe(
|
||||
startWith(0),
|
||||
switchMap(() => this.apiService.listLiquidReservesMonth$()),
|
||||
map(reserves => {
|
||||
const labels = reserves.map(stats => stats.date);
|
||||
const series = reserves.map(stats => parseFloat(stats.amount) / 100000000);
|
||||
return {
|
||||
series,
|
||||
labels
|
||||
};
|
||||
}),
|
||||
share()
|
||||
);
|
||||
|
||||
this.fullHistory$ = combineLatest([this.liquidPegsMonth$, this.currentPeg$, this.liquidReservesMonth$, this.currentReserves$])
|
||||
.pipe(
|
||||
map(([liquidPegs, currentPeg, liquidReserves, currentReserves]) => {
|
||||
liquidPegs.series[liquidPegs.series.length - 1] = parseFloat(currentPeg.amount) / 100000000;
|
||||
|
||||
|
||||
if (liquidPegs.series.length === liquidReserves?.series.length) {
|
||||
liquidReserves.series[liquidReserves.series.length - 1] = parseFloat(currentReserves?.amount) / 100000000;
|
||||
} else if (liquidPegs.series.length === liquidReserves?.series.length + 1) {
|
||||
@@ -371,7 +389,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
labels: []
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
liquidPegs,
|
||||
liquidReserves
|
||||
@@ -415,17 +433,14 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
this.incomingGraphHeight = 300;
|
||||
this.goggleResolution = 82;
|
||||
this.lbtcPegGraphHeight = 320;
|
||||
this.nbFeaturedAssets = 6;
|
||||
} else if (window.innerWidth >= 768) {
|
||||
this.incomingGraphHeight = 215;
|
||||
this.goggleResolution = 80;
|
||||
this.lbtcPegGraphHeight = 230;
|
||||
this.nbFeaturedAssets = 4;
|
||||
} else {
|
||||
this.incomingGraphHeight = 180;
|
||||
this.goggleResolution = 86;
|
||||
this.lbtcPegGraphHeight = 220;
|
||||
this.nbFeaturedAssets = 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +155,7 @@ export const restApiDocsData = [
|
||||
previousRetarget: -4.807005268478962,
|
||||
nextRetargetHeight: 741888,
|
||||
timeAvg: 302328,
|
||||
adjustedTimeAvg: 302328,
|
||||
timeOffset: 0
|
||||
}`
|
||||
},
|
||||
@@ -171,6 +172,7 @@ export const restApiDocsData = [
|
||||
previousRetarget: -4.807005268478962,
|
||||
nextRetargetHeight: 741888,
|
||||
timeAvg: 302328,
|
||||
adjustedTimeAvg: 302328,
|
||||
timeOffset: 0
|
||||
}`
|
||||
},
|
||||
@@ -187,6 +189,7 @@ export const restApiDocsData = [
|
||||
previousRetarget: -4.807005268478962,
|
||||
nextRetargetHeight: 741888,
|
||||
timeAvg: 302328,
|
||||
adjustedTimeAvg: 302328,
|
||||
timeOffset: 0
|
||||
}`
|
||||
},
|
||||
@@ -203,6 +206,7 @@ export const restApiDocsData = [
|
||||
previousRetarget: -4.807005268478962,
|
||||
nextRetargetHeight: 741888,
|
||||
timeAvg: 302328,
|
||||
adjustedTimeAvg: 302328,
|
||||
timeOffset: 0
|
||||
}`
|
||||
}
|
||||
@@ -1013,7 +1017,7 @@ export const restApiDocsData = [
|
||||
fragment: "get-address-transactions",
|
||||
title: "GET Address Transactions",
|
||||
description: {
|
||||
default: "Get transaction history for the specified address/scripthash, sorted with newest first. Returns up to 50 mempool transactions plus the first 25 confirmed transactions. You can request more confirmed transactions using <code>:last_seen_txid</code> (see below)."
|
||||
default: "Get transaction history for the specified address/scripthash, sorted with newest first. Returns up to 50 mempool transactions plus the first 25 confirmed transactions. You can request more confirmed transactions using an <code>after_txid</code> query parameter."
|
||||
},
|
||||
urlString: "/address/:address/txs",
|
||||
showConditions: bitcoinNetworks.concat(liquidNetworks),
|
||||
@@ -10066,8 +10070,7 @@ export const restApiDocsData = [
|
||||
"id": 89,
|
||||
"user_id": 1,
|
||||
"txid": "ae2639469ec000ed1d14e2550cbb01794e1cd288a00cdc7cce18398ba3cc2ffe",
|
||||
"status": "failed",
|
||||
"estimated_fee": 247,
|
||||
"status": "failed"
|
||||
"fee_paid": 0,
|
||||
"added": 1706378712,
|
||||
"last_updated": 1706378712,
|
||||
@@ -10096,8 +10099,7 @@ export const restApiDocsData = [
|
||||
"id": 88,
|
||||
"user_id": 1,
|
||||
"txid": "c5840e89173331760e959a190b24e2a289121277ed7f8a095fe289b37cee9fde",
|
||||
"status": "completed",
|
||||
"estimated_fee": 223,
|
||||
"status": "completed"
|
||||
"fee_paid": 140019,
|
||||
"added": 1706378704,
|
||||
"last_updated": 1706380231,
|
||||
@@ -10126,8 +10128,7 @@ export const restApiDocsData = [
|
||||
"id": 87,
|
||||
"user_id": 1,
|
||||
"txid": "178b5b9b310f0d667d7ea563a2cdcc17bc8cd15261b58b1653860a724ca83458",
|
||||
"status": "completed",
|
||||
"estimated_fee": 115,
|
||||
"status": "completed"
|
||||
"fee_paid": 90062,
|
||||
"added": 1706378684,
|
||||
"last_updated": 1706380231,
|
||||
|
||||
@@ -12,6 +12,13 @@ import { FeeDistributionGraphComponent } from '../components/fee-distribution-gr
|
||||
import { IncomingTransactionsGraphComponent } from '../components/incoming-transactions-graph/incoming-transactions-graph.component';
|
||||
import { MempoolGraphComponent } from '../components/mempool-graph/mempool-graph.component';
|
||||
import { LbtcPegsGraphComponent } from '../components/lbtc-pegs-graph/lbtc-pegs-graph.component';
|
||||
import { ReservesSupplyStatsComponent } from '../components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component';
|
||||
import { ReservesRatioStatsComponent } from '../components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component';
|
||||
import { ReservesRatioComponent } from '../components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component';
|
||||
import { RecentPegsStatsComponent } from '../components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component';
|
||||
import { RecentPegsListComponent } from '../components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component';
|
||||
import { FederationAddressesStatsComponent } from '../components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component';
|
||||
import { FederationAddressesListComponent } from '../components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component';
|
||||
import { GraphsComponent } from '../components/graphs/graphs.component';
|
||||
import { StatisticsComponent } from '../components/statistics/statistics.component';
|
||||
import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.component';
|
||||
@@ -48,6 +55,13 @@ import { CommonModule } from '@angular/common';
|
||||
IncomingTransactionsGraphComponent,
|
||||
MempoolGraphComponent,
|
||||
LbtcPegsGraphComponent,
|
||||
ReservesSupplyStatsComponent,
|
||||
ReservesRatioStatsComponent,
|
||||
ReservesRatioComponent,
|
||||
RecentPegsStatsComponent,
|
||||
RecentPegsListComponent,
|
||||
FederationAddressesStatsComponent,
|
||||
FederationAddressesListComponent,
|
||||
HashrateChartComponent,
|
||||
HashrateChartPoolsComponent,
|
||||
BlockHealthGraphComponent,
|
||||
|
||||
@@ -54,6 +54,7 @@ export interface DifficultyAdjustment {
|
||||
previousTime: number;
|
||||
nextRetargetHeight: number;
|
||||
timeAvg: number;
|
||||
adjustedTimeAvg: number;
|
||||
timeOffset: number;
|
||||
expectedBlocks: number;
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ export class NodesMap implements OnInit, OnChanges {
|
||||
node.public_key,
|
||||
node.alias,
|
||||
node.capacity,
|
||||
node.active_channel_count,
|
||||
node.channels,
|
||||
node.country,
|
||||
node.iso_code,
|
||||
]);
|
||||
|
||||
@@ -64,8 +64,8 @@
|
||||
<th class="channels text-right" i18n="lightning.channels">Channels</th>
|
||||
<th class="city text-right" i18n="lightning.location">Location</th>
|
||||
</thead>
|
||||
<tbody *ngIf="nodes$ | async as countryNodes; else skeleton">
|
||||
<tr *ngFor="let node of countryNodes.nodes; let i= index; trackBy: trackByPublicKey">
|
||||
<tbody *ngIf="nodesPagination$ | async as countryNodes; else skeleton">
|
||||
<tr *ngFor="let node of countryNodes; let i= index; trackBy: trackByPublicKey">
|
||||
<td class="alias text-left text-truncate">
|
||||
<a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
|
||||
</td>
|
||||
@@ -116,5 +116,10 @@
|
||||
</ng-template>
|
||||
|
||||
</table>
|
||||
|
||||
<ngb-pagination *ngIf="nodes$ | async as countryNodes" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
|
||||
[collectionSize]="countryNodes.nodes.length" [rotate]="true" [maxSize]="maxSize" [pageSize]="pageSize" [(page)]="page"
|
||||
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
|
||||
</ngb-pagination>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,14 +22,14 @@
|
||||
|
||||
.timestamp-first {
|
||||
width: 20%;
|
||||
@media (max-width: 576px) {
|
||||
@media (max-width: 1060px) {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
|
||||
.timestamp-update {
|
||||
width: 16%;
|
||||
@media (max-width: 576px) {
|
||||
@media (max-width: 1060px) {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
.city {
|
||||
max-width: 150px;
|
||||
@media (max-width: 576px) {
|
||||
@media (max-width: 675px) {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { map, Observable, share } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest, map, Observable, share, tap } from 'rxjs';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { getFlagEmoji } from '../../shared/common.utils';
|
||||
@@ -15,6 +15,12 @@ import { GeolocationData } from '../../shared/components/geolocation/geolocation
|
||||
export class NodesPerCountry implements OnInit {
|
||||
nodes$: Observable<any>;
|
||||
country: {name: string, flag: string};
|
||||
nodesPagination$: Observable<any>;
|
||||
startingIndexSubject: BehaviorSubject<number> = new BehaviorSubject(0);
|
||||
page = 1;
|
||||
pageSize = 15;
|
||||
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
|
||||
isLoading = true;
|
||||
|
||||
skeletonLines: number[] = [];
|
||||
|
||||
@@ -23,7 +29,7 @@ export class NodesPerCountry implements OnInit {
|
||||
private seoService: SeoService,
|
||||
private route: ActivatedRoute,
|
||||
) {
|
||||
for (let i = 0; i < 20; ++i) {
|
||||
for (let i = 0; i < this.pageSize; ++i) {
|
||||
this.skeletonLines.push(i);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +37,7 @@ export class NodesPerCountry implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.nodes$ = this.apiService.getNodeForCountry$(this.route.snapshot.params.country)
|
||||
.pipe(
|
||||
tap(() => this.isLoading = true),
|
||||
map(response => {
|
||||
this.seoService.setTitle($localize`Lightning nodes in ${response.country.en}`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.nodes-country:Explore all the Lightning nodes hosted in ${response.country.en} and see an overview of each node's capacity, number of open channels, and more.`);
|
||||
@@ -87,11 +94,21 @@ export class NodesPerCountry implements OnInit {
|
||||
ispCount: Object.keys(isps).length
|
||||
};
|
||||
}),
|
||||
tap(() => this.isLoading = false),
|
||||
share()
|
||||
);
|
||||
|
||||
this.nodesPagination$ = combineLatest([this.nodes$, this.startingIndexSubject]).pipe(
|
||||
map(([response, startingIndex]) => response.nodes.slice(startingIndex, startingIndex + this.pageSize))
|
||||
);
|
||||
}
|
||||
|
||||
trackByPublicKey(index: number, node: any): string {
|
||||
return node.public_key;
|
||||
}
|
||||
|
||||
pageChange(page: number): void {
|
||||
this.startingIndexSubject.next((page - 1) * this.pageSize);
|
||||
this.page = page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,8 +61,8 @@
|
||||
<th class="channels text-right" i18n="lightning.channels">Channels</th>
|
||||
<th class="city text-right" i18n="lightning.location">Location</th>
|
||||
</thead>
|
||||
<tbody *ngIf="nodes$ | async as ispNodes; else skeleton">
|
||||
<tr *ngFor="let node of ispNodes.nodes; let i= index; trackBy: trackByPublicKey">
|
||||
<tbody *ngIf="nodesPagination$ | async as ispNodes; else skeleton">
|
||||
<tr *ngFor="let node of ispNodes; let i= index; trackBy: trackByPublicKey">
|
||||
<td class="alias text-left text-truncate">
|
||||
<a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
|
||||
</td>
|
||||
@@ -113,5 +113,10 @@
|
||||
</ng-template>
|
||||
|
||||
</table>
|
||||
|
||||
<ngb-pagination *ngIf="nodes$ | async as ispNodes" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
|
||||
[collectionSize]="ispNodes.nodes.length" [rotate]="true" [maxSize]="maxSize" [pageSize]="pageSize" [(page)]="page"
|
||||
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
|
||||
</ngb-pagination>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
.timestamp-first {
|
||||
width: 20%;
|
||||
|
||||
@media (max-width: 576px) {
|
||||
@media (max-width: 1060px) {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@
|
||||
.timestamp-update {
|
||||
width: 16%;
|
||||
|
||||
@media (max-width: 576px) {
|
||||
@media (max-width: 1060px) {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@
|
||||
.city {
|
||||
max-width: 150px;
|
||||
|
||||
@media (max-width: 576px) {
|
||||
@media (max-width: 675px) {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { map, Observable, share } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest, map, Observable, share, tap } from 'rxjs';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { getFlagEmoji } from '../../shared/common.utils';
|
||||
@@ -15,6 +15,12 @@ import { GeolocationData } from '../../shared/components/geolocation/geolocation
|
||||
export class NodesPerISP implements OnInit {
|
||||
nodes$: Observable<any>;
|
||||
isp: {name: string, id: number};
|
||||
nodesPagination$: Observable<any>;
|
||||
startingIndexSubject: BehaviorSubject<number> = new BehaviorSubject(0);
|
||||
page = 1;
|
||||
pageSize = 15;
|
||||
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
|
||||
isLoading = true;
|
||||
|
||||
skeletonLines: number[] = [];
|
||||
|
||||
@@ -23,7 +29,7 @@ export class NodesPerISP implements OnInit {
|
||||
private seoService: SeoService,
|
||||
private route: ActivatedRoute,
|
||||
) {
|
||||
for (let i = 0; i < 20; ++i) {
|
||||
for (let i = 0; i < this.pageSize; ++i) {
|
||||
this.skeletonLines.push(i);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +37,7 @@ export class NodesPerISP implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.nodes$ = this.apiService.getNodeForISP$(this.route.snapshot.params.isp)
|
||||
.pipe(
|
||||
tap(() => this.isLoading = true),
|
||||
map(response => {
|
||||
this.isp = {
|
||||
name: response.isp,
|
||||
@@ -77,11 +84,21 @@ export class NodesPerISP implements OnInit {
|
||||
topCountry: topCountry,
|
||||
};
|
||||
}),
|
||||
tap(() => this.isLoading = false),
|
||||
share()
|
||||
);
|
||||
|
||||
this.nodesPagination$ = combineLatest([this.nodes$, this.startingIndexSubject]).pipe(
|
||||
map(([response, startingIndex]) => response.nodes.slice(startingIndex, startingIndex + this.pageSize))
|
||||
);
|
||||
}
|
||||
|
||||
trackByPublicKey(index: number, node: any): string {
|
||||
return node.public_key;
|
||||
}
|
||||
|
||||
pageChange(page: number): void {
|
||||
this.startingIndexSubject.next((page - 1) * this.pageSize);
|
||||
this.page = page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,17 +15,10 @@ import { AssetsComponent } from '../components/assets/assets.component';
|
||||
import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component'
|
||||
import { AssetComponent } from '../components/asset/asset.component';
|
||||
import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component';
|
||||
import { ReservesAuditDashboardComponent } from '../components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component';
|
||||
import { ReservesSupplyStatsComponent } from '../components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component';
|
||||
import { RecentPegsStatsComponent } from '../components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component';
|
||||
import { RecentPegsListComponent } from '../components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component';
|
||||
import { FederationWalletComponent } from '../components/liquid-reserves-audit/federation-wallet/federation-wallet.component';
|
||||
import { FederationUtxosListComponent } from '../components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component';
|
||||
import { FederationAddressesStatsComponent } from '../components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component';
|
||||
import { FederationAddressesListComponent } from '../components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component';
|
||||
import { ReservesRatioComponent } from '../components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component';
|
||||
import { ReservesRatioStatsComponent } from '../components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component';
|
||||
import { ReservesRatioGraphComponent } from '../components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -77,18 +70,6 @@ const routes: Routes = [
|
||||
data: { preload: true, networkSpecific: true },
|
||||
loadChildren: () => import('../components/block/block.module').then(m => m.BlockModule),
|
||||
},
|
||||
{
|
||||
path: 'audit',
|
||||
data: { networks: ['liquid'] },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
data: { networks: ['liquid'] },
|
||||
component: ReservesAuditDashboardComponent,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'audit/wallet',
|
||||
data: { networks: ['liquid'] },
|
||||
@@ -180,17 +161,8 @@ export class LiquidRoutingModule { }
|
||||
],
|
||||
declarations: [
|
||||
LiquidMasterPageComponent,
|
||||
ReservesAuditDashboardComponent,
|
||||
ReservesSupplyStatsComponent,
|
||||
RecentPegsStatsComponent,
|
||||
RecentPegsListComponent,
|
||||
FederationWalletComponent,
|
||||
FederationUtxosListComponent,
|
||||
FederationAddressesStatsComponent,
|
||||
FederationAddressesListComponent,
|
||||
ReservesRatioComponent,
|
||||
ReservesRatioStatsComponent,
|
||||
ReservesRatioGraphComponent,
|
||||
]
|
||||
})
|
||||
export class LiquidMasterPageModule { }
|
||||
@@ -200,16 +200,20 @@ export class ApiService {
|
||||
return this.httpClient.get<FederationUtxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos');
|
||||
}
|
||||
|
||||
recentPegOuts$(): Observable<RecentPeg[]> {
|
||||
return this.httpClient.get<RecentPeg[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegouts');
|
||||
recentPegsList$(count: number = 0): Observable<RecentPeg[]> {
|
||||
return this.httpClient.get<RecentPeg[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/list/' + count);
|
||||
}
|
||||
|
||||
federationAddressesOneMonthAgo$(): Observable<any> {
|
||||
return this.httpClient.get<FederationAddress[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/addresses/previous-month');
|
||||
pegsCount$(): Observable<any> {
|
||||
return this.httpClient.get<number>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/count');
|
||||
}
|
||||
|
||||
federationUtxosOneMonthAgo$(): Observable<any> {
|
||||
return this.httpClient.get<FederationUtxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos/previous-month');
|
||||
federationAddressesNumber$(): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/addresses/total');
|
||||
}
|
||||
|
||||
federationUtxosNumber$(): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos/total');
|
||||
}
|
||||
|
||||
listFeaturedAssets$(): Observable<any[]> {
|
||||
|
||||
@@ -139,6 +139,14 @@ export class EnterpriseService {
|
||||
this.getMatomo()?.trackGoal(id);
|
||||
}
|
||||
|
||||
page() {
|
||||
const matomo = this.getMatomo();
|
||||
if (matomo) {
|
||||
matomo.setCustomUrl(this.getCustomUrl());
|
||||
matomo.trackPageView();
|
||||
}
|
||||
}
|
||||
|
||||
private getCustomUrl(): string {
|
||||
let url = window.location.origin + '/';
|
||||
let route = this.activatedRoute;
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
<p><a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a></p>
|
||||
<p><a [routerLink]="['/privacy-policy']" i18n="shared.privacy-policy|Privacy Policy">Privacy Policy</a></p>
|
||||
<p><a [routerLink]="['/trademark-policy']" i18n="shared.trademark-policy|Trademark Policy">Trademark Policy</a></p>
|
||||
<p><a [routerLink]="['/3rdpartylicenses.txt']" i18n="shared.trademark-policy|Third-party Licenses">Third-party Licenses</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row social-links">
|
||||
|
||||
@@ -21,7 +21,7 @@ export const TransactionFlags = {
|
||||
no_rbf: 0b00000010n,
|
||||
v1: 0b00000100n,
|
||||
v2: 0b00001000n,
|
||||
multisig: 0b00010000n,
|
||||
v3: 0b00010000n,
|
||||
// address types
|
||||
p2pk: 0b00000001_00000000n,
|
||||
p2ms: 0b00000010_00000000n,
|
||||
@@ -34,6 +34,7 @@ export const TransactionFlags = {
|
||||
cpfp_parent: 0b00000001_00000000_00000000n,
|
||||
cpfp_child: 0b00000010_00000000_00000000n,
|
||||
replacement: 0b00000100_00000000_00000000n,
|
||||
acceleration: 0b00001000_00000000_00000000n,
|
||||
// data
|
||||
op_return: 0b00000001_00000000_00000000_00000000n,
|
||||
fake_pubkey: 0b00000010_00000000_00000000_00000000n,
|
||||
@@ -64,7 +65,7 @@ export const TransactionFilters: { [key: string]: Filter } = {
|
||||
no_rbf: { key: 'no_rbf', label: 'RBF disabled', flag: TransactionFlags.no_rbf, toggle: 'rbf', important: true },
|
||||
v1: { key: 'v1', label: 'Version 1', flag: TransactionFlags.v1, toggle: 'version' },
|
||||
v2: { key: 'v2', label: 'Version 2', flag: TransactionFlags.v2, toggle: 'version' },
|
||||
// multisig: { key: 'multisig', label: 'Multisig', flag: TransactionFlags.multisig },
|
||||
v3: { key: 'v3', label: 'Version 3', flag: TransactionFlags.v3, toggle: 'version' },
|
||||
/* address types */
|
||||
p2pk: { key: 'p2pk', label: 'P2PK', flag: TransactionFlags.p2pk, important: true },
|
||||
p2ms: { key: 'p2ms', label: 'Bare multisig', flag: TransactionFlags.p2ms, important: true },
|
||||
@@ -77,6 +78,7 @@ export const TransactionFilters: { [key: string]: Filter } = {
|
||||
cpfp_parent: { key: 'cpfp_parent', label: 'Paid for by child', flag: TransactionFlags.cpfp_parent, important: true },
|
||||
cpfp_child: { key: 'cpfp_child', label: 'Pays for parent', flag: TransactionFlags.cpfp_child, important: true },
|
||||
replacement: { key: 'replacement', label: 'Replacement', flag: TransactionFlags.replacement, important: true },
|
||||
acceleration: window?.['__env']?.ACCELERATOR ? { key: 'acceleration', label: 'Accelerated', flag: TransactionFlags.acceleration, important: false } : undefined,
|
||||
/* data */
|
||||
op_return: { key: 'op_return', label: 'OP_RETURN', flag: TransactionFlags.op_return, important: true },
|
||||
fake_pubkey: { key: 'fake_pubkey', label: 'Fake pubkey', flag: TransactionFlags.fake_pubkey },
|
||||
@@ -94,9 +96,9 @@ export const TransactionFilters: { [key: string]: Filter } = {
|
||||
};
|
||||
|
||||
export const FilterGroups: { label: string, filters: Filter[]}[] = [
|
||||
{ label: 'Features', filters: ['rbf', 'no_rbf', 'v1', 'v2', 'multisig'] },
|
||||
{ label: 'Features', filters: ['rbf', 'no_rbf', 'v1', 'v2', 'v3'] },
|
||||
{ label: 'Address Types', filters: ['p2pk', 'p2ms', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'] },
|
||||
{ label: 'Behavior', filters: ['cpfp_parent', 'cpfp_child', 'replacement'] },
|
||||
{ label: 'Behavior', filters: ['cpfp_parent', 'cpfp_child', 'replacement', 'acceleration'] },
|
||||
{ label: 'Data', filters: ['op_return', 'fake_pubkey', 'inscription'] },
|
||||
{ label: 'Heuristics', filters: ['coinjoin', 'consolidation', 'batch_payout'] },
|
||||
{ label: 'Sighash Flags', filters: ['sighash_all', 'sighash_none', 'sighash_single', 'sighash_default', 'sighash_acp'] },
|
||||
|
||||
@@ -7,7 +7,6 @@ discover=1
|
||||
par=16
|
||||
dbcache=8192
|
||||
maxmempool=4096
|
||||
mempoolexpiry=999999
|
||||
mempoolfullrbf=1
|
||||
maxconnections=100
|
||||
onion=127.0.0.1:9050
|
||||
@@ -20,6 +19,7 @@ whitelist=2401:b140::/32
|
||||
#uacomment=@wiz
|
||||
|
||||
[main]
|
||||
mempoolexpiry=999999
|
||||
rpcbind=127.0.0.1:8332
|
||||
rpcbind=[::1]:8332
|
||||
bind=0.0.0.0:8333
|
||||
|
||||
@@ -64,15 +64,7 @@
|
||||
"http://node203.tk7.mempool.space:3001",
|
||||
"http://node204.tk7.mempool.space:3001",
|
||||
"http://node205.tk7.mempool.space:3001",
|
||||
"http://node206.tk7.mempool.space:3001",
|
||||
"http://node207.tk7.mempool.space:3001",
|
||||
"http://node208.tk7.mempool.space:3001",
|
||||
"http://node209.tk7.mempool.space:3001",
|
||||
"http://node210.tk7.mempool.space:3001",
|
||||
"http://node211.tk7.mempool.space:3001",
|
||||
"http://node212.tk7.mempool.space:3001",
|
||||
"http://node213.tk7.mempool.space:3001",
|
||||
"http://node214.tk7.mempool.space:3001"
|
||||
"http://node206.tk7.mempool.space:3001"
|
||||
]
|
||||
},
|
||||
"DATABASE": {
|
||||
|
||||
@@ -64,15 +64,7 @@
|
||||
"http://node203.tk7.mempool.space:3004",
|
||||
"http://node204.tk7.mempool.space:3004",
|
||||
"http://node205.tk7.mempool.space:3004",
|
||||
"http://node206.tk7.mempool.space:3004",
|
||||
"http://node207.tk7.mempool.space:3004",
|
||||
"http://node208.tk7.mempool.space:3004",
|
||||
"http://node209.tk7.mempool.space:3004",
|
||||
"http://node210.tk7.mempool.space:3004",
|
||||
"http://node211.tk7.mempool.space:3004",
|
||||
"http://node212.tk7.mempool.space:3004",
|
||||
"http://node213.tk7.mempool.space:3004",
|
||||
"http://node214.tk7.mempool.space:3004"
|
||||
"http://node206.tk7.mempool.space:3004"
|
||||
]
|
||||
},
|
||||
"DATABASE": {
|
||||
|
||||
@@ -57,15 +57,7 @@
|
||||
"http://node203.tk7.mempool.space:3000",
|
||||
"http://node204.tk7.mempool.space:3000",
|
||||
"http://node205.tk7.mempool.space:3000",
|
||||
"http://node206.tk7.mempool.space:3000",
|
||||
"http://node207.tk7.mempool.space:3000",
|
||||
"http://node208.tk7.mempool.space:3000",
|
||||
"http://node209.tk7.mempool.space:3000",
|
||||
"http://node210.tk7.mempool.space:3000",
|
||||
"http://node211.tk7.mempool.space:3000",
|
||||
"http://node212.tk7.mempool.space:3000",
|
||||
"http://node213.tk7.mempool.space:3000",
|
||||
"http://node214.tk7.mempool.space:3000"
|
||||
"http://node206.tk7.mempool.space:3000"
|
||||
]
|
||||
},
|
||||
"LIGHTNING": {
|
||||
|
||||
@@ -78,15 +78,7 @@
|
||||
"http://node203.tk7.mempool.space:3000",
|
||||
"http://node204.tk7.mempool.space:3000",
|
||||
"http://node205.tk7.mempool.space:3000",
|
||||
"http://node206.tk7.mempool.space:3000",
|
||||
"http://node207.tk7.mempool.space:3000",
|
||||
"http://node208.tk7.mempool.space:3000",
|
||||
"http://node209.tk7.mempool.space:3000",
|
||||
"http://node210.tk7.mempool.space:3000",
|
||||
"http://node211.tk7.mempool.space:3000",
|
||||
"http://node212.tk7.mempool.space:3000",
|
||||
"http://node213.tk7.mempool.space:3000",
|
||||
"http://node214.tk7.mempool.space:3000"
|
||||
"http://node206.tk7.mempool.space:3000"
|
||||
]
|
||||
},
|
||||
"DATABASE": {
|
||||
@@ -145,15 +137,7 @@
|
||||
"node203.tk7.mempool.space",
|
||||
"node204.tk7.mempool.space",
|
||||
"node205.tk7.mempool.space",
|
||||
"node206.tk7.mempool.space",
|
||||
"node207.tk7.mempool.space",
|
||||
"node208.tk7.mempool.space",
|
||||
"node209.tk7.mempool.space",
|
||||
"node210.tk7.mempool.space",
|
||||
"node211.tk7.mempool.space",
|
||||
"node212.tk7.mempool.space",
|
||||
"node213.tk7.mempool.space",
|
||||
"node214.tk7.mempool.space"
|
||||
"node206.tk7.mempool.space"
|
||||
]
|
||||
},
|
||||
"REDIS": {
|
||||
|
||||
@@ -57,15 +57,7 @@
|
||||
"http://node203.tk7.mempool.space:3003",
|
||||
"http://node204.tk7.mempool.space:3003",
|
||||
"http://node205.tk7.mempool.space:3003",
|
||||
"http://node206.tk7.mempool.space:3003",
|
||||
"http://node207.tk7.mempool.space:3003",
|
||||
"http://node208.tk7.mempool.space:3003",
|
||||
"http://node209.tk7.mempool.space:3003",
|
||||
"http://node210.tk7.mempool.space:3003",
|
||||
"http://node211.tk7.mempool.space:3003",
|
||||
"http://node212.tk7.mempool.space:3003",
|
||||
"http://node213.tk7.mempool.space:3003",
|
||||
"http://node214.tk7.mempool.space:3003"
|
||||
"http://node206.tk7.mempool.space:3003"
|
||||
]
|
||||
},
|
||||
"LIGHTNING": {
|
||||
|
||||
@@ -66,15 +66,7 @@
|
||||
"http://node203.tk7.mempool.space:3003",
|
||||
"http://node204.tk7.mempool.space:3003",
|
||||
"http://node205.tk7.mempool.space:3003",
|
||||
"http://node206.tk7.mempool.space:3003",
|
||||
"http://node207.tk7.mempool.space:3003",
|
||||
"http://node208.tk7.mempool.space:3003",
|
||||
"http://node209.tk7.mempool.space:3003",
|
||||
"http://node210.tk7.mempool.space:3003",
|
||||
"http://node211.tk7.mempool.space:3003",
|
||||
"http://node212.tk7.mempool.space:3003",
|
||||
"http://node213.tk7.mempool.space:3003",
|
||||
"http://node214.tk7.mempool.space:3003"
|
||||
"http://node206.tk7.mempool.space:3003"
|
||||
]
|
||||
},
|
||||
"DATABASE": {
|
||||
|
||||
@@ -57,15 +57,7 @@
|
||||
"http://node203.tk7.mempool.space:3002",
|
||||
"http://node204.tk7.mempool.space:3002",
|
||||
"http://node205.tk7.mempool.space:3002",
|
||||
"http://node206.tk7.mempool.space:3002",
|
||||
"http://node207.tk7.mempool.space:3002",
|
||||
"http://node208.tk7.mempool.space:3002",
|
||||
"http://node209.tk7.mempool.space:3002",
|
||||
"http://node210.tk7.mempool.space:3002",
|
||||
"http://node211.tk7.mempool.space:3002",
|
||||
"http://node212.tk7.mempool.space:3002",
|
||||
"http://node213.tk7.mempool.space:3002",
|
||||
"http://node214.tk7.mempool.space:3002"
|
||||
"http://node206.tk7.mempool.space:3002"
|
||||
]
|
||||
},
|
||||
"LIGHTNING": {
|
||||
|
||||
@@ -66,15 +66,7 @@
|
||||
"http://node203.tk7.mempool.space:3002",
|
||||
"http://node204.tk7.mempool.space:3002",
|
||||
"http://node205.tk7.mempool.space:3002",
|
||||
"http://node206.tk7.mempool.space:3002",
|
||||
"http://node207.tk7.mempool.space:3002",
|
||||
"http://node208.tk7.mempool.space:3002",
|
||||
"http://node209.tk7.mempool.space:3002",
|
||||
"http://node210.tk7.mempool.space:3002",
|
||||
"http://node211.tk7.mempool.space:3002",
|
||||
"http://node212.tk7.mempool.space:3002",
|
||||
"http://node213.tk7.mempool.space:3002",
|
||||
"http://node214.tk7.mempool.space:3002"
|
||||
"http://node206.tk7.mempool.space:3002"
|
||||
]
|
||||
},
|
||||
"DATABASE": {
|
||||
|
||||
@@ -20,5 +20,10 @@ for pid in `ps uaxww|grep warmer|grep zsh|awk '{print $2}'`;do
|
||||
kill $pid
|
||||
done
|
||||
|
||||
# kill nginx cache heater scripts
|
||||
for pid in `ps uaxww|grep heater|grep zsh|awk '{print $2}'`;do
|
||||
kill $pid
|
||||
done
|
||||
|
||||
# always exit successfully despite above errors
|
||||
exit 0
|
||||
|
||||
6
production/nginx/http-acl.conf
Normal file
6
production/nginx/http-acl.conf
Normal file
@@ -0,0 +1,6 @@
|
||||
# used for "internal" API restriction
|
||||
geo $remote_addr $mempool_external {
|
||||
127.0.0.1 '';
|
||||
::1 '';
|
||||
default 1;
|
||||
}
|
||||
@@ -4,10 +4,17 @@
|
||||
|
||||
# Block the internal APIs of esplora
|
||||
location /api/internal/ {
|
||||
return 403;
|
||||
if ($mempool_external) {
|
||||
return 403;
|
||||
}
|
||||
rewrite ^/api/(.*) /$1 break;
|
||||
try_files /dev/null @esplora-api-cache-disabled;
|
||||
}
|
||||
location /api/v1/internal/ {
|
||||
return 403;
|
||||
if ($mempool_external) {
|
||||
return 403;
|
||||
}
|
||||
try_files /dev/null @mempool-api-v1-cache-normal;
|
||||
}
|
||||
|
||||
# websocket has special HTTP headers
|
||||
|
||||
@@ -19,6 +19,7 @@ http {
|
||||
|
||||
# HTTP basic configuration
|
||||
include mempool/production/nginx/http-basic.conf;
|
||||
include mempool/production/nginx/http-acl.conf;
|
||||
include mempool/production/nginx/http-proxy-cache.conf;
|
||||
include mempool/production/nginx/http-language.conf;
|
||||
|
||||
|
||||
@@ -251,7 +251,8 @@ class Server {
|
||||
|
||||
if (!img) {
|
||||
// send local fallback image file
|
||||
res.sendFile(nodejsPath.join(__dirname, matchedRoute.fallbackFile));
|
||||
res.set('Cache-control', 'no-cache');
|
||||
res.sendFile(nodejsPath.join(__dirname, matchedRoute.fallbackImg));
|
||||
} else {
|
||||
res.contentType('image/png');
|
||||
res.send(img);
|
||||
|
||||
1
unfurler/src/resources
Symbolic link
1
unfurler/src/resources
Symbolic link
@@ -0,0 +1 @@
|
||||
../../frontend/src/resources
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 94 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 289 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 96 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user