Compare commits

..

72 Commits

Author SHA1 Message Date
wiz
018d4230e2 ops: Remove node207-214.tk7 from prod cluster configs 2024-02-29 19:33:39 +09:00
softsimon
6c66da5426 Merge pull request #4720 from mempool/dependabot/npm_and_yarn/backend/babel/core-7.24.0
Bump @babel/core from 7.23.2 to 7.24.0 in /backend
2024-02-29 17:20:18 +07:00
softsimon
5b9b0d0968 Merge pull request #4694 from mempool/dependabot/npm_and_yarn/frontend/echarts-5.5.0
Bump echarts from 5.4.3 to 5.5.0 in /frontend
2024-02-29 12:05:10 +07:00
softsimon
ddf1bc0654 Merge pull request #4721 from mempool/simon/accel-tab-mainnet-only
Only display acceleration tab on mainnet
2024-02-29 11:55:08 +07:00
softsimon
419b40884a Only display acceleration tab on mainnet
fixes #4719
2024-02-29 11:49:12 +07:00
dependabot[bot]
1f05e7b366 Bump @babel/core from 7.23.2 to 7.24.0 in /backend
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.23.2 to 7.24.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.24.0/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-29 02:32:53 +00:00
softsimon
a9d94bdafd Merge pull request #4685 from mempool/mononaut/v3-filter
Add Goggles filter for nVersion=3
2024-02-27 15:44:13 +07:00
wiz
18b1805ff3 Merge pull request #4706 from mempool/natsoni/fix-liquid-dashboard-safari
Fix liquid dashboard css
2024-02-27 12:22:18 +09:00
wiz
4044ca3877 Merge pull request #4708 from mempool/mononaut/acceleration-goggles
Add Acceleration Goggles
2024-02-27 12:21:55 +09:00
wiz
66386f2bcc Merge pull request #4709 from mempool/hunicus/txs-api-queryparam
Fix query parameter label in tx history api doc
2024-02-27 12:21:17 +09:00
wiz
d0bf26bcef Merge pull request #4711 from mempool/nymkappa/update-doc
[doc] remove "estimated_fee" from accelerator endpoints
2024-02-27 12:20:54 +09:00
wiz
e918e1fdab ops: Implement ACL for internal APIs 2024-02-27 11:46:14 +09:00
nymkappa
968a26d2cd Merge branch 'master' into nymkappa/update-doc 2024-02-25 10:07:48 +01:00
wiz
43fde86e9d Merge pull request #4710 from mempool/knorrium/update_node_matrix_20_21
Update node matrix to v20 and v21
2024-02-25 16:48:27 +09:00
wiz
587c7f6d59 ops: Kill nginx cache heater script on shutdown 2024-02-25 16:47:40 +09:00
Felipe Knorr Kuhn
f431ca742f Update node matrix to v20 and v21 2024-02-24 23:40:36 -08:00
wiz
7e16c5e8c4 Tweak a few strings on Acceleration Summary screen 2024-02-25 16:18:43 +09:00
hunicus
8701ba8546 Fix query parameter label in tx history api doc 2024-02-25 01:17:46 -05:00
Mononaut
42912ff007 Add Acceleration Goggles 2024-02-24 20:37:41 +00:00
natsoni
6d8a72301c Fix recent pegs table css 2024-02-22 15:14:47 +01:00
natsoni
1cdbde680c Fix liquid dashboard on mobile view 2024-02-22 14:43:19 +01:00
wiz
ec21b3d06f Merge pull request #4678 from mempool/knorrium/skip_latest_tagging
Temporarily disable tagging images as latest
2024-02-22 19:18:55 +09:00
wiz
e8e43c074b Merge pull request #4704 from mempool/mononaut/matomo-events
matomo events
2024-02-22 19:18:27 +09:00
nymkappa
dc50b40b69 if matomo is undefined, don't try to use it 2024-02-22 11:07:50 +01:00
wiz
e41d6bc2e6 Merge pull request #4504 from mempool/orangesurf/trademark-updates-2
Move 3rd party licenses to footer
2024-02-22 17:45:27 +09:00
wiz
cb7938baa1 Merge branch 'master' into orangesurf/trademark-updates-2 2024-02-22 15:56:08 +09:00
wiz
4d6b62c7de Merge pull request #4205 from mempool/mononaut/unfurl-fallback-no-cache
Unfurl fallback no-cache
2024-02-22 15:52:10 +09:00
wiz
7bf053afe5 Merge branch 'master' into mononaut/unfurl-fallback-no-cache 2024-02-22 15:51:57 +09:00
wiz
d8900d40c7 Merge pull request #4204 from mempool/mononaut/unfurl-symlink-fallbacks
Use symlink to avoid duplicate unfurler fallback images
2024-02-22 15:51:47 +09:00
wiz
841f18f481 Merge pull request #4687 from mempool/mononaut/testnet-expiry
Restore mempoolexpiry on test networks
2024-02-22 15:43:24 +09:00
wiz
b0d34d4e48 Merge branch 'master' into mononaut/matomo-events 2024-02-22 15:27:57 +09:00
Mononaut
c684834c42 matomo tx events 2024-02-21 23:15:31 +00:00
softsimon
46e989ab27 Merge pull request #4692 from mempool/natsoni/fix-goggles-z-index
Fix block visualisation tooltip z-index
2024-02-21 12:17:43 +07:00
softsimon
2c19e44fbf Merge pull request #4690 from mempool/natsoni/fix-hardcoded-hashrate-unit
Fix hardcoded hashrate unit in pool ranking graph
2024-02-21 11:12:21 +07:00
softsimon
dab72f669d Merge pull request #4700 from mempool/natsoni/lightning-pagination
Add pagination to Lightning nodes list and fix table overflow
2024-02-21 11:07:35 +07:00
softsimon
16537e8018 Merge pull request #4703 from mempool/mononaut/revert-halving-countdown
Revert reverted halving countdown widget
2024-02-21 11:05:52 +07:00
Mononaut
a3b713cc74 Revert reverted halving countdown widget 2024-02-20 23:12:41 +00:00
natsoni
17697e669a Add pagination to Lightning nodes list and fix overflow 2024-02-19 17:08:07 +01:00
softsimon
035068a72e Merge pull request #4632 from mempool/mononaut/redis-error-handling
Handle Redis errors and disconnects
2024-02-19 22:18:24 +07:00
softsimon
086ae6978a Merge branch 'master' into mononaut/redis-error-handling 2024-02-19 21:21:05 +07:00
softsimon
951b946be9 Merge pull request #4699 from mempool/natsoni/fix-nodes-map-undefined-channels
Fix nodes map undefined channels count
2024-02-19 21:19:21 +07:00
natsoni
5fc2205b54 Fix nodes map undefined channels count 2024-02-19 11:57:43 +01:00
softsimon
18e412939d Merge pull request #4698 from mempool/mononaut/bg-goggles-indexing
Continue other indexing tasks while Goggles classification runs
2024-02-19 12:29:46 +08:00
Mononaut
69dc21d232 Continue other indexing tasks while Goggles classification runs 2024-02-19 04:11:03 +00:00
dependabot[bot]
521d2fc650 Bump echarts from 5.4.3 to 5.5.0 in /frontend
Bumps [echarts](https://github.com/apache/echarts) from 5.4.3 to 5.5.0.
- [Release notes](https://github.com/apache/echarts/releases)
- [Commits](https://github.com/apache/echarts/compare/5.4.3...5.5.0)

---
updated-dependencies:
- dependency-name: echarts
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-19 02:25:26 +00:00
natsoni
ac250ebe6c Add z-index to block overview tooltip component 2024-02-17 18:08:22 +01:00
natsoni
596aada169 Fix hardcoded hashrate unit in pool ranking graph 2024-02-16 09:59:37 +01:00
nymkappa
c4df05b581 [doc] remove "estimated_fee" from accelerator endpoints 2024-02-15 17:33:38 +01:00
softsimon
7fb699a02b Merge pull request #4680 from mempool/mononaut/sliding-difficulty
Project early difficulty adjustment with sliding window
2024-02-15 23:55:21 +08:00
Mononaut
e8fc1e8522 Always show estimated difficulty adjustment 2024-02-15 15:32:31 +00:00
softsimon
5f14b32a06 Merge branch 'master' into mononaut/sliding-difficulty 2024-02-15 22:57:21 +08:00
softsimon
e2d7c82553 Merge pull request #4673 from mempool/natsoni/liquid-dashboard-minor-fixes
Liquid Audit dashboard: requested changes
2024-02-15 17:13:20 +08:00
natsoni
7259a00c8d Polish pegometer for varying screen width 2024-02-14 18:22:04 +01:00
Mononaut
7c6c1187bb Restore mempoolexpiry on test networks 2024-02-14 15:13:00 +00:00
natsoni
9f4c351e3d Minor logic change in dashboard component 2024-02-13 16:55:02 +01:00
softsimon
329c1330f4 Merge pull request #4686 from mempool/move-sponsor-boxes
Move sponsor boxes to subscriptions account panel
2024-02-13 21:26:51 +08:00
natsoni
d55d5db01d Resolve conflicts in dashboard component 2024-02-13 09:47:45 +01:00
natsoni
55cd2f5678 Liquid dashboard: switch places of top graphs 2024-02-13 09:36:22 +01:00
natsoni
69747a0028 Migrate audit dashboard to main Liquid page 2024-02-13 00:59:28 +01:00
hunicus
45c1e05ebc Move sponsor boxes to subscriptions account panel 2024-02-12 17:43:34 -05:00
Mononaut
8713d33d1a Add Goggles filter for nVersion=3 2024-02-12 20:00:57 +00:00
natsoni
99aa6c9ed3 Liquid: improve recent pegs pagination data query 2024-02-12 11:34:47 +01:00
Mononaut
000524691a Project early difficulty from sliding window 2024-02-11 23:11:19 +00:00
Felipe Knorr Kuhn
290de43df0 Temporarily disable tagging images as latest 2024-02-11 13:10:08 -08:00
natsoni
33ac4056d8 Display percent instead of ratio in stats 2024-02-09 18:59:24 +01:00
natsoni
4ef1df47a3 Fix addresses stat error 2024-02-09 18:46:34 +01:00
natsoni
346c024ddf Change federation wallet stats to include utxos count 2024-02-09 16:26:59 +01:00
natsoni
42bd08c744 Minor fixes on Liquid dashboards 2024-02-09 14:26:34 +01:00
Mononaut
ba8ea7d0d1 Handle Redis errors and disconnects 2024-01-30 01:10:36 +00:00
orangesurf
445d600633 Changes following feedback 2023-12-15 10:37:16 +00:00
Mononaut
ab8386adea Set no-cache on unfurl failure fallback response 2023-08-23 02:10:57 +09:00
Mononaut
b7474b29e4 Use symlink to avoid duplicate fallback images 2023-08-23 00:34:55 +09:00
104 changed files with 1226 additions and 1803 deletions

View File

@@ -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"

View File

@@ -100,6 +100,5 @@ jobs:
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform linux/amd64,linux/arm64 \
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \
--output "type=registry" ./${{ matrix.service }}/ \
--build-arg commitHash=$SHORT_SHA

View File

@@ -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": {

View File

@@ -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",

View File

@@ -137,10 +137,6 @@
"AUDIT_START_HEIGHT": 774000,
"SERVERS": []
},
"REBROADCAST": {
"ENABLED": false,
"FREQUENCY": 3600
},
"MEMPOOL_SERVICES": {
"API": "",
"ACCELERATIONS": false

View File

@@ -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]);
}
});

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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 } = {};

View File

@@ -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
);
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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));

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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));

View File

@@ -36,7 +36,6 @@ class Logger {
mining: 'Mining',
ln: 'Lightning',
goggles: 'Goggles',
rebroadcaster: 'Rebroadcaster',
};
// @ts-ignore

View File

@@ -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,

View File

@@ -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__

View File

@@ -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

View File

@@ -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"
},

View File

@@ -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",

View File

@@ -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>

View File

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

View File

@@ -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) {
}

View File

@@ -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>

View File

@@ -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 }}

View File

@@ -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;

View File

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

View File

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

View File

@@ -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;
})

View File

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

View File

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

View File

@@ -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">

View File

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

View File

@@ -1,4 +1,4 @@
<div [ngClass]="{'widget': widget}">
<div [ngClass]="{'widget': widget, 'extra-margin-right': widget}">
<div class="clearfix"></div>

View File

@@ -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;
}
}
}

View File

@@ -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>&nbsp;<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
</a>
<div class="card-text">
<div class="fee-text">{{ 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>&nbsp;<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
</a>
<div class="card-text">
<div class="fee-text">{{ federationWalletStats.address_count }} <span i18n="shared.addresses">addresses</span></div>
<div class="fiat">{{ federationWalletStats.utxo_count }} <span i18n="shared.utxos">UTXOs</span></div>
</div>
</div>
</div>
</div>
<ng-template #loadingData>
@@ -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>

View File

@@ -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%;

View File

@@ -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}
})
)
}
}

View File

@@ -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">
&lrm;{{ peg.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="symbol lg-inline relative-time"><i>(<app-time kind="since" [time]="peg.blocktime"></app-time>)</i></div>
</td>
<td class="amount text-right" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0, 'glow-effect': peg.amount < 0 && peg.bitcoinaddress && !peg.bitcointxid}">
<app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount>
</td>
<td class="output text-left">
<ng-container *ngIf="peg.bitcointxid; else redeemInProgress">
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/tx/' + peg.bitcointxid + ':' + peg.bitcoinindex }}" target="_blank" style="color:#b86d12">
<app-truncate [text]="peg.bitcointxid + ':' + peg.bitcoinindex" [lastChars]="6"></app-truncate>
</a>
</ng-container>
<ng-template #redeemInProgress>
<ng-container *ngIf="peg.bitcoinaddress; else noRedeem">
<i><span class="text-muted" i18n="liquid.redemption-in-progress">Peg out in progress...</span></i>
</ng-container>
</ng-template>
</td>
<td class="address text-left">
<ng-container *ngIf="peg.bitcoinaddress; else noRedeem">
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + peg.bitcoinaddress }}" target="_blank" style="color:#b86d12">
<app-truncate [text]="peg.bitcoinaddress" [lastChars]="6"></app-truncate>
</a>
</ng-container>
</td>
</tr>
</ng-template>
</tbody>
<ng-template #skeleton>
<tbody *ngIf="widget; else regularRowsSkeleton">
<tr *ngFor="let item of skeletonLines">
<td class="transaction text-left widget">
<span class="skeleton-loader" style="max-width: 400px"></span>
</td>
<td class="timestamp text-left widget">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
<td class="amount text-right widget">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
</tr>
</tbody>
<ng-template #regularRowsSkeleton>
<tr *ngFor="let item of skeletonLines">
<td class="transaction text-left">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
<td class="timestamp text-left">
<span class="skeleton-loader" style="max-width: 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">
&lrm;{{ peg.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="symbol lg-inline relative-time"><i>(<app-time kind="since" [time]="peg.blocktime"></app-time>)</i></div>
</td>
<td class="amount text-right" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0, 'glow-effect': peg.amount < 0 && peg.bitcoinaddress && !peg.bitcointxid}">
<app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount>
</td>
<td class="output text-left">
<ng-container *ngIf="peg.bitcointxid; else redeemInProgress">
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/tx/' + peg.bitcointxid + ':' + peg.bitcoinindex }}" target="_blank" style="color:#b86d12">
<app-truncate [text]="peg.bitcointxid + ':' + peg.bitcoinindex" [lastChars]="6"></app-truncate>
</a>
</ng-container>
<ng-template #redeemInProgress>
<ng-container *ngIf="peg.bitcoinaddress; else noRedeem">
<i><span class="text-muted" i18n="liquid.redemption-in-progress">Peg out in progress...</span></i>
</ng-container>
</ng-template>
</td>
<td class="address text-left">
<ng-container *ngIf="peg.bitcoinaddress; else noRedeem">
<a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + peg.bitcoinaddress }}" target="_blank" style="color:#b86d12">
<app-truncate [text]="peg.bitcoinaddress" [lastChars]="6"></app-truncate>
</a>
</ng-container>
</td>
</tr>
</ng-template>
</tbody>
<ng-template #skeleton>
<tbody *ngIf="widget; else regularRowsSkeleton">
<tr *ngFor="let item of skeletonLines">
<td class="transaction text-left widget">
<span class="skeleton-loader" style="max-width: 400px"></span>
</td>
<td class="timestamp text-left widget">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
<td class="amount text-right widget">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
</tr>
</tbody>
<ng-template #regularRowsSkeleton>
<tr *ngFor="let item of skeletonLines">
<td class="transaction text-left">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
<td class="timestamp text-left">
<span class="skeleton-loader" style="max-width: 240px"></span>
</td>
<td class="amount text-right">
<span class="skeleton-loader" style="max-width: 140px"></span>
</td>
<td class="output text-left">
<span class="skeleton-loader" style="max-width: 300px"></span>
</td>
<td class="address text-left">
<span class="skeleton-loader" style="max-width: 240px"></span>
</td>
</tr>
</ng-template>
</ng-template>
</table>
<ngb-pagination *ngIf="!widget && pegsCount$ | async as pegsCount" class="pagination-container float-right mt-2" [class]="isLoading || isPegCountLoading ? 'disabled' : ''"
[collectionSize]="pegsCount" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page"
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
</ngb-pagination>
<ng-template [ngIf]="!widget">
<div class="clearfix"></div>
<br>
</ng-template>
</div>
</div>
<br>

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -1,6 +1,7 @@
.fee-estimation-container {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
@media (min-width: 376px) {
flex-direction: row;
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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>

View File

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

View File

@@ -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'
}
},
};
}
}

View File

@@ -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();
}
}

View File

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

View File

@@ -41,6 +41,7 @@
}
.fee-text{
border-bottom: 1px solid #ffffff1c;
color: #ffffff;
width: fit-content;
margin: auto;
line-height: 1.45;

View File

@@ -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>

View File

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

View File

@@ -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>` +

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
</a>
<div class="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>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
</a>
<div class="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>&nbsp;<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>

View File

@@ -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;
}

View File

@@ -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;
}
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -54,6 +54,7 @@ export interface DifficultyAdjustment {
previousTime: number;
nextRetargetHeight: number;
timeAvg: number;
adjustedTimeAvg: number;
timeOffset: number;
expectedBlocks: number;
}

View File

@@ -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,
]);

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -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;
}
}

View File

@@ -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 { }

View File

@@ -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[]> {

View File

@@ -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;

View File

@@ -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">

View File

@@ -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'] },

View File

@@ -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

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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

View File

@@ -0,0 +1,6 @@
# used for "internal" API restriction
geo $remote_addr $mempool_external {
127.0.0.1 '';
::1 '';
default 1;
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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
View File

@@ -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

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