Compare commits

...

307 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
softsimon
512f632475 Merge pull request #4676 from mempool/simon/log-mempool-on-new-blocks
Log mempool stats on new blocks
2024-02-12 23:07:49 +08:00
softsimon
6dedecad70 Merge pull request #4681 from mempool/mononaut/fix-log-stats-on-new-block
Fix statistics timespans
2024-02-12 18:55:48 +08:00
natsoni
99aa6c9ed3 Liquid: improve recent pegs pagination data query 2024-02-12 11:34:47 +01:00
Mononaut
d24d643d70 Fix statistics timespan for 2h and 24hr charts 2024-02-12 00:54:34 +00: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
softsimon
cc2f42e814 Merge pull request #4672 from mempool/nymkappa/fix-username-not-showing
[auth] properly refresh user when `auth` object changes
2024-02-11 20:13:51 +08:00
softsimon
a40727b194 Merge pull request #4624 from mempool/nymkappa/api-key-rest
[doc] add accelerator rest api documentation
2024-02-11 20:02:41 +08:00
nymkappa
6933d3c395 Merge branch 'master' into nymkappa/fix-username-not-showing 2024-02-11 12:03:04 +01:00
nymkappa
4fc5a6197a Merge branch 'master' into nymkappa/api-key-rest 2024-02-11 12:01:38 +01:00
nymkappa
fbabd93b5e Update frontend/src/app/docs/api-docs/api-docs-data.ts
Co-authored-by: hunicus <93150691+hunicus@users.noreply.github.com>
2024-02-11 12:01:09 +01:00
softsimon
2c22200820 Log mempool stats on new blocks 2024-02-10 20:45:58 +08:00
softsimon
d921c3fdc2 Merge pull request #4670 from mempool/mononaut/optimize-mempool-websocket
Reduce the network size of mempool block websocket updates
2024-02-10 20:10:42 +08:00
nymkappa
a1bd84ea6d [doc] separate private/public acceleration history endpoint title 2024-02-10 11:41:30 +01:00
nymkappa
740f9af9af [doc] additional rest api doc for accelerator 2024-02-10 11:40:19 +01:00
nymkappa
be183ada0a [doc] fix acceleration history endpoint detail 2024-02-10 10:56:19 +01:00
nymkappa
a2b01587b1 [auth] small refactor 2024-02-10 10:31:56 +01:00
nymkappa
5f698dfbbb Merge branch 'master' into nymkappa/api-key-rest 2024-02-10 10:23:17 +01:00
softsimon
46f2509ca0 Merge branch 'master' into mononaut/optimize-mempool-websocket 2024-02-10 15:27:15 +08:00
wiz
8bd4a8a9ee Merge pull request #4674 from mempool/mononaut/data-filter 2024-02-09 17:53:15 -05:00
wiz
e00bc86bd1 Merge pull request #4675 from mempool/mononaut/no-webgl 2024-02-09 17:52:54 -05:00
Mononaut
ade256efc7 Handle missing webgl on dashboards 2024-02-09 21:46:42 +00: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
Mononaut
e23de97e0f 💩 => data 2024-02-09 17:42:20 +00:00
natsoni
346c024ddf Change federation wallet stats to include utxos count 2024-02-09 16:26:59 +01:00
nymkappa
bb43599493 [auth] remove debug log 2024-02-09 15:53:20 +01:00
nymkappa
e6266ecedc [auth] properly refresh user when auth object changes 2024-02-09 15:49:28 +01:00
softsimon
d4db628c3b Merge pull request #4666 from mempool/mononaut/persistent-filters
Make dashboard filters persistent, add disjunctive filter mode
2024-02-09 21:31:09 +08:00
softsimon
1e56ac094c Smaller mobile toggle buttons 2024-02-09 21:30:50 +08:00
Mononaut
ddee5f927c Make dashboard filters persistent, add disjunctive filter mode 2024-02-09 21:30:50 +08:00
natsoni
42bd08c744 Minor fixes on Liquid dashboards 2024-02-09 14:26:34 +01:00
softsimon
dfbec0ceef Merge pull request #4511 from mempool/nymkappa/fix-platinium-typo
[typo] platinium -> platinum
2024-02-09 20:46:19 +08:00
nymkappa
3b3081f884 Merge branch 'master' into nymkappa/api-key-rest 2024-02-09 12:51:30 +01:00
softsimon
925d51f40e Merge pull request #4669 from mempool/mononaut/fix-dashboard-sizes
Fix mining/liquid dashboard widget heights
2024-02-09 14:11:43 +08:00
softsimon
b3dbe1215c Adjusting Load more position to match other dashboards 2024-02-09 14:11:12 +08:00
softsimon
dd89c398cc Merge pull request #4665 from mempool/natsoni/fix-liquid-dashboard-mobile
Fix Liquid dashboard layout on mobile view
2024-02-09 14:01:29 +08:00
Mononaut
7de4a03f1b Fix mining/liquid dashboard widget heights 2024-02-08 22:50:47 +00:00
Mononaut
1121136a5e Reduce the network size of mempool block websocket updates 2024-02-08 22:40:22 +00:00
natsoni
e5f0544cc2 Fix Liquid dashboard layout for mobile view 2024-02-08 18:32:07 +01:00
nymkappa
3456d2737a Merge branch 'master' into nymkappa/fix-platinium-typo 2024-02-08 15:11:45 +01:00
softsimon
efdc83d4bb Enabling Accelerator 2024-02-08 21:25:34 +08:00
softsimon
d745bf3f9e Merge pull request #4664 from mempool/simon/liquid-mining-dashboard-heights
Fixed liquid and mining dashboard graph heights
2024-02-08 21:02:58 +08:00
softsimon
9d6231b6e5 New mining dashboard graph height 2024-02-08 21:01:24 +08:00
softsimon
fdad3d1fd5 Fixing new liquid dashboard height 2024-02-08 20:56:16 +08:00
wiz
1e3650594c Merge pull request #4509 from mempool/nymkappa/mega-branch
[Mega branch] Various fixes and improvements
2024-02-08 07:55:25 -05:00
nymkappa
2dc2a22edb Merge branch 'master' into nymkappa/mega-branch 2024-02-08 13:37:17 +01:00
softsimon
dcccc3c8a2 Merge pull request #4658 from mempool/natsoni/fix-undefined-block-fee
Blockchain component: display dash when feerate is undefined
2024-02-08 20:31:00 +08:00
wiz
49553b7bae Merge pull request #4653 from mempool/mononaut/healthier-checks
Staggered esplora fallback health checks
2024-02-08 07:23:39 -05:00
softsimon
01e019245b Merge pull request #4651 from mempool/natsoni/liquid-audit-improvements
Liquid audit dashboard: various improvements
2024-02-08 19:50:55 +08:00
softsimon
01066eae64 Merge pull request #4648 from mempool/natsoni/search-bar-confidential-addresses
Search bar: add Liquid confidential addresses to addresses regex
2024-02-08 19:45:08 +08:00
softsimon
b88d6bc7b7 Merge pull request #4659 from mempool/natsoni/fix-broken-blocks-skeleton
Fix broken skeleton loader of blocks-list
2024-02-08 18:34:00 +08:00
softsimon
a51d82bcfb Merge branch 'master' into natsoni/fix-broken-blocks-skeleton 2024-02-08 16:22:06 +08:00
softsimon
999994c701 Merge pull request #4652 from mempool/mononaut/fix-address-scroll
Fix infinite address scroll
2024-02-08 16:14:05 +08:00
softsimon
0a7c91a4e9 Merge pull request #4663 from mempool/mononaut/dashboard-concept
New dashboard concept
2024-02-08 14:57:44 +08:00
softsimon
510dd91328 Changed to toggle buttons. Changed filters. 2024-02-08 14:52:31 +08:00
Mononaut
3ceb583127 Fix Goggles title alignment 2024-02-08 04:18:05 +00:00
Mononaut
d7be5a4737 Simple Goggle Toggles 2024-02-08 04:17:54 +00:00
Mononaut
3039481686 Change titles, adjust padding 2024-02-08 02:47:46 +00:00
Mononaut
f14cd5ee2b Swap incoming vb chart and mempool stats 2024-02-08 00:37:16 +00:00
Mononaut
e268a6a033 Fix mining dashboard hashrate graph height 2024-02-08 00:03:34 +00:00
Mononaut
ca2c5d3628 Expand lightning dashboard widgets & improve responsiveness 2024-02-08 00:00:06 +00:00
Mononaut
ce7a007b62 Expand mining dashboard widgets & improve responsiveness 2024-02-07 23:26:06 +00:00
Mononaut
ba0a3d004d Expand acceleration dashboard widgets & improve responsiveness 2024-02-07 21:40:22 +00:00
Mononaut
29e4a581e7 Add next block visualization to main dashboard, expand widgets 2024-02-07 21:39:39 +00:00
Mononaut
4f98a413d1 address scroll handle different page sizes 2024-02-07 20:23:35 +00:00
nymkappa
0ab1183048 Merge branch 'master' into nymkappa/mega-branch 2024-02-07 17:01:55 +01:00
natsoni
03d4375b17 Clean up blocks-list skeleton 2024-02-07 14:51:23 +01:00
softsimon
e86d0dc868 Merge pull request #4573 from mempool/dependabot/npm_and_yarn/backend/follow-redirects-1.15.5
Bump follow-redirects from 1.15.2 to 1.15.5 in /backend
2024-02-07 21:47:56 +08:00
softsimon
e9a7ea3623 Merge pull request #4650 from mempool/natsoni/add-liquid-peg-out-label
Add P2WSH Liquid peg out to input label
2024-02-07 20:09:48 +08:00
dependabot[bot]
1e568e5b4c Bump follow-redirects from 1.15.2 to 1.15.5 in /backend
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.5.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.5)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-07 11:53:08 +00:00
softsimon
b56350dca1 Merge pull request #4633 from mempool/dependabot/npm_and_yarn/backend/mysql2-3.9.1
Bump mysql2 from 3.7.0 to 3.9.1 in /backend
2024-02-07 19:52:24 +08:00
natsoni
20614213a7 Add skeleton to recent blocks on Liquid dashboard 2024-02-07 12:50:36 +01:00
softsimon
582eca1fdd Merge branch 'master' into dependabot/npm_and_yarn/backend/mysql2-3.9.1 2024-02-07 19:47:41 +08:00
natsoni
df7f1cc86b Blockchain component: display dash when feerate is undefined 2024-02-07 11:31:22 +01:00
softsimon
0acb797494 Merge pull request #4646 from mempool/knorrium/download_from_cdn
[sync-assets] More logging, add MEMPOOL_CDN and DRY_RUN options
2024-02-07 16:05:01 +08:00
Felipe Knorr Kuhn
195eeaa7b9 Merge branch 'master' into knorrium/download_from_cdn 2024-02-06 20:10:39 -08:00
softsimon
48c2317e20 Merge pull request #4654 from jamesblacklock/docker-bugfix-2
fix syntax error in mempool-config.json
2024-02-07 12:06:04 +08:00
Felipe Knorr Kuhn
95233f0ef3 Merge branch 'master' into docker-bugfix-2 2024-02-06 19:56:22 -08:00
softsimon
29c6efa9ed Merge pull request #4657 from mempool/knorrium/validate_final_docker_json
Add a step to validate the JSON config generated for Docker
2024-02-07 11:55:42 +08:00
Felipe Knorr Kuhn
624cbf05c0 Add a step to validate the JSON config generated for Docker 2024-02-06 19:47:09 -08:00
James Blacklock
1547c2c477 fix syntax error in mempool-config.json 2024-02-06 22:13:10 -05:00
Mononaut
3959f52d19 Staggered esplora fallback health checks 2024-02-06 23:42:14 +00:00
Mononaut
dfdd286f75 Fix infinite address scroll 2024-02-06 21:50:29 +00:00
wiz
fea115bcbc ops: Fix obvious bug in cache heater script 2024-02-06 14:03:44 -05:00
wiz
f004705896 ops: Add missing HTTP Expires header for fees API 2024-02-06 13:55:44 -05:00
wiz
87a613e4dc ops: Fix broken nginx cache configuration 2024-02-06 13:53:03 -05:00
natsoni
b561648082 Add peg ins/out volume to stats components 2024-02-06 17:35:02 +01:00
natsoni
602c87bea0 Update peg-in/out links to open new page on click 2024-02-06 17:34:24 +01:00
natsoni
b453898b1d Add glow effect to peg outs in progress 2024-02-06 17:34:04 +01:00
Felipe Knorr Kuhn
c4c886b584 Merge branch 'master' into knorrium/download_from_cdn 2024-02-06 07:44:00 -08:00
natsoni
517d36783f Add P2WSH Liquid peg out to input label 2024-02-06 11:37:51 +01:00
nymkappa
f964888c47 Merge branch 'master' into nymkappa/mega-branch 2024-02-05 17:10:07 +01:00
natsoni
c58115a96c Add confidential addresses in liquid address regex 2024-02-05 11:53:05 +01:00
natsoni
fc415372bf Fix P2SH / P2PKH detection on Liquid address page 2024-02-05 11:22:14 +01:00
softsimon
47d221fd3b Merge pull request #4647 from mempool/mononaut/better-coinjoin-goggles
Improve Goggles coinjoin detection
2024-02-05 18:13:12 +08:00
Mononaut
7e5e85c8b9 Improve Goggles coinjoin detection 2024-02-04 21:47:31 +00:00
Felipe Knorr Kuhn
9c72d1df3b Sync assets from the mempool CDN 2024-02-04 09:18:50 -08:00
Felipe Knorr Kuhn
933bc47b44 Add DRY_RUN and MEMPOOL_CDN options 2024-02-04 09:17:42 -08:00
softsimon
a15729c38f Merge pull request #4640 from mempool/fix-liquid-frontend-crash
Fix Liquid frontend crash
2024-02-04 23:38:32 +08:00
softsimon
229e122daf Merge branch 'master' into fix-liquid-frontend-crash 2024-02-04 23:14:35 +08:00
softsimon
16846d526c Merge pull request #4616 from mempool/natsee/liquid-audit-dashboard
Feature: Liquid Federation Audit Dashboard
2024-02-04 22:05:02 +08:00
natsoni
19a4c10860 Fix date alignment in utxos table 2024-02-04 14:57:04 +01:00
natsoni
ae38c3bf81 Update CLA for natsoni 2024-02-04 13:06:49 +01:00
natsoni
575c407098 Remove SQL transactions in federation utxos indexing 2024-02-04 13:03:41 +01:00
natsoni
03e4a21bce Fix styling issue in peg and utxo widgets 2024-02-04 13:01:07 +01:00
natsoni
7bfc649042 Add seo title to pegs and utxos tables 2024-02-04 12:34:40 +01:00
Felipe Knorr Kuhn
e478fb2279 Merge branch 'master' into fix-liquid-frontend-crash 2024-02-03 09:17:53 -08:00
softsimon
9494d2fb0b Merge pull request #4631 from mempool/mononaut/fix-null-err
Fix missing optional chain on bestDescendant access
2024-02-03 21:59:37 +08:00
softsimon
b44ec76130 Merge pull request #4637 from mempool/mononaut/summaries-indexing-error
More robust error handling and logging during summaries indexing
2024-02-03 20:39:09 +08:00
softsimon
dd7bd9e397 Merge pull request #4639 from mempool/mononaut/acc-data-catch
Fix error handling in acceleration data polling
2024-02-02 23:57:41 +08:00
Mononaut
0bf0d79ee4 Fix error handling in acceleration data polling 2024-02-02 15:31:10 +00:00
softsimon
788a8693ee Merge pull request #4523 from jamesblacklock/docker-bugfix
fix bug in backend Docker `start.sh` script
2024-02-02 11:40:45 +08:00
natsee
163c0b6d78 Liquid peg outs address indexing: support for duplicates and minor fixes 2024-02-01 19:29:31 +01:00
natsee
7ab5b2a2b7 Add check for mempoolBlocks length in mempool subscription 2024-02-01 12:13:34 +01:00
nymkappa
f3fd70a846 Merge branch 'master' into nymkappa/mega-branch 2024-01-30 23:17:26 +01:00
James Blacklock
770b6dfd19 Merge branch 'master' into docker-bugfix 2024-01-30 15:50:03 -05:00
Mononaut
db8ba7c938 More robust error handling and logging during summaries indexing 2024-01-30 16:42:55 +00:00
wiz
e4d92c8fe0 Merge pull request #4629 from mempool/wiz/add-new-prod-servers
ops: Add new prod servers for fallback/replication
2024-01-30 10:18:10 -05:00
natsee
1e15428a63 Liquid Audit: fix i18n 2024-01-30 15:11:51 +01:00
natsee
f4de3a44e0 Liquid: Add address column in recent pegs table 2024-01-30 14:25:08 +01:00
natsee
e69ba7e884 Liquid Federation indexing: clean up logging 2024-01-30 11:55:59 +01:00
softsimon
b4906a5e8a Merge pull request #4626 from mempool/mononaut/fix-pool-dominance
Fix pools dominance chart
2024-01-30 17:21:49 +07:00
natsee
73e6045549 Liquid: Add support for peg outs display 2024-01-30 11:11:30 +01:00
dependabot[bot]
5f3f483546 Bump mysql2 from 3.7.0 to 3.9.1 in /backend
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.7.0 to 3.9.1.
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/compare/v3.7.0...v3.9.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 02:07:07 +00:00
Mononaut
ba8ea7d0d1 Handle Redis errors and disconnects 2024-01-30 01:10:36 +00:00
Mononaut
e458196bff Fix missing optional chain on bestDescendant access 2024-01-29 23:15:21 +00:00
Mononaut
2637636f27 Fix pool dominance sorting 2024-01-29 16:59:13 +00:00
natsee
639fc3dd5f Liquid audit: Add recent pegs widget and table 2024-01-29 17:06:25 +01:00
natsee
451a61e5fc Reformat UTXOs widget to Peg Ins widget 2024-01-29 12:22:47 +01:00
nymkappa
b2a130bb17 [doc] fix copy, formatting and hide accelerator doc title if non official instance 2024-01-29 11:45:14 +01:00
natsee
c39ca0270e Fix elements_pegs bitcoinaddress field 2024-01-29 11:36:58 +01:00
natsee
b6e5a059c9 Liquid: minor changes on audit display 2024-01-29 11:06:51 +01:00
hunicus
a3055e20f4 Fix signet and testnet docs pages 2024-01-29 01:57:06 -05:00
wiz
3ac70b3860 ops: Add new prod servers for fallback/replication 2024-01-29 00:05:16 -05:00
natsee
71f73835da Liquid Federation: filter out change UTXOs from table 2024-01-28 21:46:31 +01:00
natsee
59128c8ca0 Audit dashboard: merge utxos and addresses tables in same page 2024-01-28 20:07:41 +01:00
natsee
36d3734d55 Liquid Federation dashboard: minor changes 2024-01-28 18:26:00 +01:00
softsimon
c9c477c88f Merge pull request #4622 from mempool/mononaut/beta-accelerator
Add beta tag to accelerator dashboard nav item
2024-01-28 17:39:51 +07:00
softsimon
8806b8332b Merge pull request #4621 from mempool/mononaut/acceleration-list
Fix acceleration list css, move to /acceleration/list
2024-01-28 17:37:24 +07:00
softsimon
c7a38c78d2 Merge pull request #4619 from mempool/nymkappa/halving-widget
[mining] update halving widget to be more human readable
2024-01-28 17:31:05 +07:00
nymkappa
2d189ca3f6 Merge branch 'master' into nymkappa/halving-widget 2024-01-28 09:50:39 +01:00
softsimon
4b798730b1 Merge pull request #4623 from mempool/mononaut/goggles-index-config
Goggles indexing config & rate limit
2024-01-28 15:24:45 +07:00
Mononaut
bbb0aea0d1 Fix pools dominance chart 2024-01-27 22:25:42 +00:00
nymkappa
57c7df4f1c Merge pull request #4625 from mempool/mononaut/double-time-units
dual-unit app-time halving countdown
2024-01-27 20:25:34 +01:00
Mononaut
87910a6eb7 Display halving countdown with double units 2024-01-27 19:11:43 +00:00
nymkappa
ea1b74dcef [doc] add accelerator rest api documentation 2024-01-27 19:35:16 +01:00
Mononaut
09e38ac177 Goggles indexing config & rate limit 2024-01-27 18:22:58 +00:00
natsee
5b93d39d73 Fix duplicate api call to audit status 2024-01-27 19:19:14 +01:00
Mononaut
4e5ebabc54 Add beta tag to accelerator dashboard tab 2024-01-27 18:03:53 +00:00
Mononaut
a79e1aec1c Fix acceleration list css, move to /acceleration/list 2024-01-27 16:45:18 +00:00
nymkappa
085d40a4e3 Merge branch 'master' into nymkappa/mega-branch 2024-01-27 16:27:25 +01:00
nymkappa
057abbb6c1 [mining] update halving widget to be more human readable 2024-01-27 16:16:32 +01:00
natsee
1a2844ffdf Liquid dashboard: Fix double api call 2024-01-26 20:49:48 +01:00
natsee
7102a72f84 Remove beta tag to Liquid Federation Audit 2024-01-26 19:11:38 +01:00
natsee
87e328504f Liquid: Fix audit updating conditions 2024-01-26 18:52:07 +01:00
softsimon
520e79aec4 Merge pull request #4615 from mempool/mononaut/halvenening
Fix halving fireworks
2024-01-26 23:56:18 +07:00
softsimon
ee6422c982 Merge pull request #4609 from afahrer/master
Channel search results showing up during TX search
2024-01-26 23:43:47 +07:00
wiz
e8ca8e7ec1 ops: Add redis to prod installer 2024-01-26 10:51:44 -05:00
wiz
b110f86b48 ops: Add new nginx hot cacher 2024-01-26 10:43:03 -05:00
wiz
5a7a78cddc ops: Add new nginx hot cacher 2024-01-26 10:40:21 -05:00
wiz
c7862e19c2 ops: Add /api/v1/fees/recommended to warm cache 2024-01-26 10:26:42 -05:00
softsimon
3610908171 Update rust-gbt package lock version 2024-01-26 21:48:21 +07:00
softsimon
87c336734d Merge pull request #4612 from mempool/mononaut/update-rust
Update rust-toolchain to 1.65
2024-01-26 21:45:40 +07:00
softsimon
bad3992397 Merge pull request #4505 from natsee/fix-overflow-pool-ranking
Fix overflow on mining pool ranking table
2024-01-26 16:13:29 +07:00
nymkappa
edb4c8d9ad Merge branch 'master' into fix-overflow-pool-ranking 2024-01-25 22:33:29 +01:00
nymkappa
565a881f3b Merge branch 'master' into nymkappa/mega-branch 2024-01-25 18:49:13 +01:00
natsee
d1d8f77b29 Fix eslint 2024-01-25 15:56:25 +01:00
natsee
c957692726 Merge branch 'natsee/liquid-federation-audit' into natsee/liquid-audit-dashboard 2024-01-25 15:22:47 +01:00
natsee
16f0830f2c Fix conflict in database-migration 2024-01-25 15:19:46 +01:00
natsee
b6d2008e97 Merge branch 'master' into natsee/liquid-federation-audit 2024-01-25 15:17:51 +01:00
natsee
a6b584d964 Liquid: Federation Audit Dashboard 2024-01-25 15:08:15 +01:00
softsimon
e81d1e273a Merge pull request #4550 from mempool/orangesurf/trademark0103
update trademarks
2024-01-25 15:46:12 +07:00
Mononaut
dbe7ebe530 Fix halving fireworks 2024-01-25 01:51:01 +00:00
Mononaut
4e007acd84 Update rust-toolchain to 1.65 2024-01-24 18:53:59 +00:00
softsimon
4fe745817c Merge pull request #4137 from mempool/mononaut/multi-address-tracking
Multi address tracking
2024-01-24 23:14:00 +07:00
softsimon
09ce1bd458 Merge pull request #4608 from mempool/mononaut/goggle-index-bugfix
Fix version update & error handling in Goggles indexing
2024-01-24 23:08:31 +07:00
Mononaut
09a727c00d Fix more Goggles indexing bugs 2024-01-24 12:18:42 +00:00
afahrer
f7f1eb067b $ match. fix transaction starting with number 2024-01-23 22:56:17 -05:00
afahrer
74d94579f9 Channel search results showing up during TX search 2024-01-23 20:48:49 -05:00
orangesurf
885331fe4a Remove space from footer 2024-01-23 18:34:43 +00:00
Mononaut
8ca2b2b5c0 Fix version update & error handling in Goggles indexing 2024-01-23 17:20:57 +00:00
softsimon
58f143f867 Merge pull request #4605 from mempool/mononaut/goggle-index
Goggle Indexing
2024-01-24 00:16:24 +07:00
softsimon
a19ffb884f Fixing broken info-link 2024-01-24 00:15:10 +07:00
softsimon
2faca121d3 Merge pull request #4526 from mempool/mononaut/mined-goggles
Mined Goggles
2024-01-24 00:13:43 +07:00
softsimon
8f99d2ba58 Merge pull request #4604 from mempool/simon/clear-tx-cache
Clear txCache when switching network
2024-01-23 23:47:40 +07:00
nymkappa
585732e438 Merge branch 'master' into orangesurf/trademark0103 2024-01-23 17:32:58 +01:00
Mononaut
0cddfcfa64 Fix missing quotes on multi-address-tracking error msg 2024-01-23 14:04:15 +00:00
Mononaut
aa0948a154 Add multi-address-tracking limit config 2024-01-23 13:36:42 +00:00
Mononaut
8f7895cb2e Track dropped txs in multi-address subscription 2024-01-23 13:36:42 +00:00
Mononaut
77a526b91c Normalize scriptpubkeys to lowercase 2024-01-23 13:36:42 +00:00
Mononaut
ccd9642a01 Deduplicate address validation code 2024-01-23 13:36:41 +00:00
Mononaut
8642956dc2 Add track-scriptpubkeys websocket subscription 2024-01-23 13:36:41 +00:00
Mononaut
57c3861ca6 Support multi address websocket subscriptions 2024-01-23 13:36:41 +00:00
softsimon
de7db08ae3 Merge pull request #4601 from mempool/natsee/seach-bar-liquid-assets
Search bar: suggest Liquid assets
2024-01-23 20:07:06 +07:00
softsimon
431f60d9c0 Use current network name in search result 2024-01-23 18:56:32 +07:00
softsimon
da545dbcf1 Merge branch 'master' into natsee/seach-bar-liquid-assets 2024-01-23 17:14:26 +07:00
natsee
b7feb0d43d Merge branch 'natsee/liquid-federation-audit' into natsee/liquid-audit-dashboard 2024-01-23 09:48:36 +01:00
natsee
cef060be2d Only include count in federation address and UTXO queries 2024-01-23 09:46:15 +01:00
softsimon
c570cc1e73 Merge branch 'master' into simon/clear-tx-cache 2024-01-23 11:20:38 +07:00
softsimon
6339cc8d59 Merge pull request #4597 from mempool/knorrium/cache_assets_gha
Cache build assets to prevent rate limiting
2024-01-23 11:20:06 +07:00
Felipe Knorr Kuhn
fe079a72be Clean up action 2024-01-22 20:06:27 -08:00
Felipe Knorr Kuhn
c789797d2d Revert "Remove the download artifacts step"
This reverts commit 479007da55.
2024-01-22 20:05:20 -08:00
Felipe Knorr Kuhn
479007da55 Remove the download artifacts step 2024-01-22 19:56:45 -08:00
Felipe Knorr Kuhn
f955c1b0f4 Merge branch 'master' into knorrium/cache_assets_gha 2024-01-22 16:53:19 -08:00
Felipe Knorr Kuhn
99f7201ec5 Cache build assets to prevent rate limiting 2024-01-22 16:51:08 -08:00
softsimon
5ecdf1988e Clear txCache when switching network
fixes #4603
2024-01-23 01:23:51 +07:00
natsee
392ea35d51 Skeleton audit dashboard 2024-01-22 16:03:55 +01:00
natsee
8f7cd70882 Add throttleTime to avoid too frequent calls to backend 2024-01-22 14:19:01 +01:00
natsee
fa90eb84fc Truncate elements_pegs and add primary key instead of drop/create 2024-01-22 13:59:11 +01:00
natsee
00613fe613 Liquid: suggest asset in search bar results 2024-01-22 11:45:07 +01:00
natsee
0663cc2cfa Fix search bar result overflow 2024-01-22 11:29:41 +01:00
natsee
81a09e9dba Add Liquid Peg-in column to Federation UTXOs list 2024-01-21 13:51:50 +01:00
natsee
cd713c61b3 Liquid: wip on Federation audit dashboard 2024-01-21 13:33:20 +01:00
natsee
752eba767a Liquid: add BTC reserves to L-BTC widget and make it dynamic 2024-01-21 13:19:02 +01:00
natsee
de2842b62a Add pegtxid and pegindex data to federation_txos table 2024-01-21 13:04:34 +01:00
orangesurf
a89af41c48 Update bisq-dashboard.component.ts
Switch from ® to &reg; for better compatibility
2024-01-21 11:20:11 +00:00
orangesurf
c751e10da8 Update seo.service.ts
Switch from ® to &reg; for better compatibility
2024-01-21 11:19:01 +00:00
orangesurf
2750f1789c Update index.ts
Switch from ® to &reg; for better compatibility
2024-01-21 11:17:50 +00:00
orangesurf
aaf378ed42 Update index.liquid.html
Switch from ® to &reg; for better compatibility
2024-01-21 11:15:11 +00:00
orangesurf
bc5e03deb5 Update index.bisq.html
Switch from ® to &reg; for better compatibility
2024-01-21 11:13:55 +00:00
orangesurf
8759d49daf Merge branch 'master' into orangesurf/trademark0103 2024-01-21 10:29:40 +00:00
natsee
4b10e32e73 Liquid: add indexing process of Federation utxos 2024-01-20 15:15:15 +01:00
nymkappa
59e30027a5 Merge branch 'master' into nymkappa/mega-branch 2024-01-19 09:49:00 +01:00
nymkappa
8054e2d79a [accelerator] fix success rate stat value 2024-01-18 15:54:04 +01:00
nymkappa
f6d7790512 [sponsors] allow setting up custom host for the sponsor component 2024-01-18 12:13:52 +01:00
nymkappa
0225455784 Fix merge conflict with 2013dc6d8b 2024-01-15 10:05:37 +01:00
nymkappa
ee721b3e1c Merge branch 'master' into nymkappa/mega-branch 2024-01-15 10:01:38 +01:00
James Blacklock
5f1180118a Merge branch 'master' into docker-bugfix 2024-01-04 12:10:31 -05:00
orangesurf
a0d0650e2a update trademarks 2024-01-03 19:09:59 +01:00
James Blacklock
3f305207f9 Merge branch 'master' into docker-bugfix 2023-12-25 03:27:26 -05:00
James Blacklock
8e9a187a15 Merge branch 'master' into docker-bugfix 2023-12-21 13:02:25 -05:00
James Blacklock
cd964d37e8 fix bug in backend Docker start.sh script 2023-12-20 17:03:06 -05:00
James Blacklock
ec4c418c22 sign contributor agreement 2023-12-20 17:02:12 -05:00
nymkappa
9e88f5ecf8 [typo] platinium -> platinum 2023-12-15 16:58:10 +01:00
nymkappa
041f4f6695 Merge branch 'nymkappa/remove-services-version' into nymkappa/mega-branch 2023-12-15 16:16:12 +01:00
nymkappa
70ca75ac12 Merge branch 'nymkappa/subscription-tag' into nymkappa/mega-branch 2023-12-15 16:16:06 +01:00
nymkappa
8238edb721 [menu] show og rank and active subscription in menu 2023-12-15 14:48:17 +01:00
natsee
0ac0a56ae5 Fix overflow on mining pool ranking table 2023-12-15 12:22:08 +01:00
orangesurf
445d600633 Changes following feedback 2023-12-15 10:37:16 +00:00
nymkappa
2013dc6d8b [refactoring] move servics related api calls to its own service 2023-12-12 17:24:58 +01:00
nymkappa
590188fe6d [footer] remove services version number 2023-12-12 09:17:46 +01:00
nymkappa
38479db5ca Merge branch 'master' into nymkappa/twitter-handle 2023-12-12 09:10:38 +01:00
nymkappa
ed215aa4cd [menu] only add space after @ username if username already contains @ 2023-12-12 09:07:27 +01: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
231 changed files with 6308 additions and 1501 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"
@@ -63,11 +63,104 @@ jobs:
run: npm run build
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
cache:
name: "Cache assets for builds"
runs-on: "ubuntu-latest"
steps:
- name: Checkout
uses: actions/checkout@v3
with:
path: assets
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
registry-url: "https://registry.npmjs.org"
- name: Install (Prod dependencies only)
run: npm ci --omit=dev --omit=optional
working-directory: assets/frontend
- name: Restore cached mining pool assets
continue-on-error: true
id: cache-mining-pool-restore
uses: actions/cache/restore@v4
with:
path: |
mining-pool-assets.zip
key: mining-pool-assets-cache
- name: Restore promo video assets
continue-on-error: true
id: cache-promo-video-restore
uses: actions/cache/restore@v4
with:
path: |
promo-video-assets.zip
key: promo-video-assets-cache
- name: Unzip assets before building (src/resources)
continue-on-error: true
run: unzip -o mining-pool-assets.zip -d assets/frontend/src/resources/mining-pools
- name: Unzip assets before building (src/resources)
continue-on-error: true
run: unzip -o promo-video-assets.zip -d assets/frontend/src/resources/promo-video
# - name: Unzip assets before building (dist)
# continue-on-error: true
# run: unzip assets.zip -d assets/frontend/dist/mempool/browser/resources
- name: Sync-assets
run: npm run sync-assets-dev
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MEMPOOL_CDN: 1
VERBOSE: 1
working-directory: assets/frontend
- name: Zip mining-pool assets
run: zip -jrq mining-pool-assets.zip assets/frontend/src/resources/mining-pools/*
- name: Zip promo-video assets
run: zip -jrq promo-video-assets.zip assets/frontend/src/resources/promo-video/*
- name: Upload mining pool assets as artifact
uses: actions/upload-artifact@v4
with:
name: mining-pool-assets
path: mining-pool-assets.zip
- name: Upload promo video assets as artifact
uses: actions/upload-artifact@v4
with:
name: promo-video-assets
path: promo-video-assets.zip
- name: Save mining pool assets cache
id: cache-mining-pool-save
uses: actions/cache/save@v4
with:
path: |
mining-pool-assets.zip
key: mining-pool-assets-cache
- name: Save promo video assets cache
id: cache-promo-video-save
uses: actions/cache/save@v4
with:
path: |
promo-video-assets.zip
key: promo-video-assets-cache
frontend:
needs: cache
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"
@@ -103,9 +196,171 @@ jobs:
# - name: Test
# run: npm run test
- name: Restore cached mining pool assets
continue-on-error: true
id: cache-mining-pool-restore
uses: actions/cache/restore@v4
with:
path: |
mining-pool-assets.zip
key: mining-pool-assets-cache
- name: Restore promo video assets
continue-on-error: true
id: cache-promo-video-restore
uses: actions/cache/restore@v4
with:
path: |
promo-video-assets.zip
key: promo-video-assets-cache
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: mining-pool-assets
- name: Unzip assets before building (src/resources)
run: unzip -o mining-pool-assets.zip -d ${{ matrix.node }}/${{ matrix.flavor }}/frontend/src/resources/mining-pools
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: promo-video-assets
- name: Unzip assets before building (src/resources)
run: unzip -o promo-video-assets.zip -d ${{ matrix.node }}/${{ matrix.flavor }}/frontend/src/resources/promo-video
# - name: Unzip assets before building (dist)
# run: unzip assets.zip -d ${{ matrix.node }}/${{ matrix.flavor }}/frontend/dist/mempool/browser/resources
- name: Display resulting source tree
run: ls -R
- name: Build
run: npm run build
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MEMPOOL_CDN: 1
VERBOSE: 1
e2e:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
runs-on: "ubuntu-latest"
needs: frontend
strategy:
fail-fast: false
matrix:
# module: ["mempool", "liquid", "bisq"] Disabling bisq support for now
module: ["mempool", "liquid"]
include:
- module: "mempool"
spec: |
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
cypress/e2e/testnet/*.spec.ts
- module: "liquid"
spec: |
cypress/e2e/liquid/liquid.spec.ts
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
# - module: "bisq"
# spec: |
# cypress/e2e/bisq/bisq.spec.ts
name: E2E tests for ${{ matrix.module }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
path: ${{ matrix.module }}
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 20
cache: "npm"
cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json
- name: Restore cached mining pool assets
continue-on-error: true
id: cache-mining-pool-restore
uses: actions/cache/restore@v4
with:
path: |
mining-pool-assets.zip
key: mining-pool-assets-cache
- name: Restore cached promo video assets
continue-on-error: true
id: cache-promo-video-restore
uses: actions/cache/restore@v4
with:
path: |
promo-video-assets.zip
key: promo-video-assets-cache
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: mining-pool-assets
- name: Unzip assets before building (src/resources)
run: unzip -o mining-pool-assets.zip -d ${{ matrix.module }}/frontend/src/resources/mining-pools
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: promo-video-assets
- name: Unzip assets before building (src/resources)
run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
- name: Chrome browser tests (${{ matrix.module }})
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:${{ matrix.module }}
start: npm run start:local-staging
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
parallel: true
spec: ${{ matrix.spec }}
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
env:
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
validate_docker_json:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
runs-on: "ubuntu-latest"
name: Validate generated backend Docker JSON
steps:
- name: Checkout
uses: actions/checkout@v3
with:
path: docker
- name: Install jq
run: sudo apt-get install jq -y
- name: Create new start script to run on CI
run: |
sed '$d' start.sh > start_ci.sh
working-directory: docker/docker/backend
- name: Run the script to generate the sample JSON
run: |
sh start_ci.sh
working-directory: docker/docker/backend
- name: Validate JSON syntax
run: |
cat mempool-config.json | jq
working-directory: docker/docker/backend

View File

@@ -1,64 +0,0 @@
name: Cypress Tests
on:
push:
branches: [master]
pull_request:
types: [opened, synchronize]
jobs:
cypress:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
runs-on: "ubuntu-latest"
strategy:
fail-fast: false
matrix:
module: ["mempool", "liquid", "bisq"]
include:
- module: "mempool"
spec: |
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
cypress/e2e/testnet/*.spec.ts
- module: "liquid"
spec: |
cypress/e2e/liquid/liquid.spec.ts
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
- module: "bisq"
spec: |
cypress/e2e/bisq/bisq.spec.ts
name: E2E tests for ${{ matrix.module }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
path: ${{ matrix.module }}
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 20
cache: "npm"
cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json
- name: Chrome browser tests (${{ matrix.module }})
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:${{ matrix.module }}
start: npm run start:local-staging
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
parallel: true
spec: ${{ matrix.spec }}
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
env:
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}

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

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ backend/mempool-config.json
frontend/src/resources/config.template.js
frontend/src/resources/config.js
target
docker/backend/start_ci.sh

2
Cargo.lock generated
View File

@@ -62,7 +62,7 @@ dependencies = [
[[package]]
name = "gbt"
version = "0.1.0"
version = "1.0.0"
dependencies = [
"bytemuck",
"bytes",

6
backend/.gitignore vendored
View File

@@ -7,6 +7,12 @@ mempool-config.json
pools.json
icons.json
# docker
Dockerfile
GeoIP
start.sh
wait-for-it.sh
# compiled output
/dist
/tmp

View File

@@ -16,6 +16,7 @@
"MEMPOOL_BLOCKS_AMOUNT": 8,
"INDEXING_BLOCKS_AMOUNT": 11000,
"BLOCKS_SUMMARIES_INDEXING": false,
"GOGGLES_INDEXING": false,
"USE_SECOND_NODE_FOR_MINFEE": false,
"EXTERNAL_ASSETS": [],
"EXTERNAL_MAX_RETRY": 1,
@@ -33,7 +34,8 @@
"DISK_CACHE_BLOCK_INTERVAL": 6,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true,
"PRICE_UPDATES_PER_HOUR": 1
"PRICE_UPDATES_PER_HOUR": 1,
"MAX_TRACKED_ADDRESSES": 100
},
"CORE_RPC": {
"HOST": "127.0.0.1",

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",
@@ -17,7 +16,7 @@
"crypto-js": "~4.2.0",
"express": "~4.18.2",
"maxmind": "~4.3.11",
"mysql2": "~3.7.0",
"mysql2": "~3.9.1",
"redis": "^4.6.6",
"rust-gbt": "file:./rust-gbt",
"socks-proxy-agent": "~7.0.0",
@@ -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": {
@@ -3673,9 +3672,9 @@
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"funding": [
{
"type": "individual",
@@ -6110,9 +6109,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/mysql2": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.7.0.tgz",
"integrity": "sha512-c45jA3Jc1X8yJKzrWu1GpplBKGwv/wIV6ITZTlCSY7npF2YfJR+6nMP5e+NTQhUeJPSyOQAbGDCGEHbAl8HN9w==",
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.1.tgz",
"integrity": "sha512-3njoWAAhGBYy0tWBabqUQcLtczZUxrmmtc2vszQUekg3kTJyZ5/IeLC3Fo04u6y6Iy5Sba7pIIa2P/gs8D3ZeQ==",
"dependencies": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",
@@ -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": {
@@ -7667,10 +7666,10 @@
},
"rust-gbt": {
"name": "gbt",
"version": "3.0.0-dev",
"version": "3.0.1",
"hasInstallScript": true,
"dependencies": {
"@napi-rs/cli": "^2.16.1"
"@napi-rs/cli": "2.16.1"
},
"engines": {
"node": ">= 12"
@@ -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": {
@@ -10440,9 +10439,9 @@
"dev": true
},
"follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw=="
},
"form-data": {
"version": "4.0.0",
@@ -12230,9 +12229,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"mysql2": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.7.0.tgz",
"integrity": "sha512-c45jA3Jc1X8yJKzrWu1GpplBKGwv/wIV6ITZTlCSY7npF2YfJR+6nMP5e+NTQhUeJPSyOQAbGDCGEHbAl8HN9w==",
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.1.tgz",
"integrity": "sha512-3njoWAAhGBYy0tWBabqUQcLtczZUxrmmtc2vszQUekg3kTJyZ5/IeLC3Fo04u6y6Iy5Sba7pIIa2P/gs8D3ZeQ==",
"requires": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",
@@ -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": {
@@ -12703,7 +12702,7 @@
"rust-gbt": {
"version": "file:rust-gbt",
"requires": {
"@napi-rs/cli": "^2.16.1"
"@napi-rs/cli": "2.16.1"
}
},
"safe-buffer": {

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",
@@ -47,7 +47,7 @@
"crypto-js": "~4.2.0",
"express": "~4.18.2",
"maxmind": "~4.3.11",
"mysql2": "~3.7.0",
"mysql2": "~3.9.1",
"rust-gbt": "file:./rust-gbt",
"redis": "^4.6.6",
"socks-proxy-agent": "~7.0.0",
@@ -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

@@ -1,7 +1,7 @@
[package]
name = "gbt"
version = "0.1.0"
description = "An inefficient re-implementation of the getBlockTemplate algorithm in Rust"
version = "1.0.0"
description = "An efficient re-implementation of the getBlockTemplate algorithm in Rust"
authors = ["mononaut"]
edition = "2021"
publish = false

View File

@@ -1,15 +1,15 @@
{
"name": "gbt",
"version": "3.0.0-dev",
"version": "3.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gbt",
"version": "3.0.0-dev",
"version": "3.0.1",
"hasInstallScript": true,
"dependencies": {
"@napi-rs/cli": "^2.16.1"
"@napi-rs/cli": "2.16.1"
},
"engines": {
"node": ">= 12"

View File

@@ -1,7 +1,7 @@
{
"name": "gbt",
"version": "3.0.0-dev",
"description": "An inefficient re-implementation of the getBlockTemplate algorithm in Rust",
"version": "3.0.1",
"description": "An efficient re-implementation of the getBlockTemplate algorithm in Rust",
"main": "index.js",
"types": "index.d.ts",
"scripts": {
@@ -25,7 +25,7 @@
}
},
"dependencies": {
"@napi-rs/cli": "^2.16.1"
"@napi-rs/cli": "2.16.1"
},
"engines": {
"node": ">= 12"

View File

@@ -4,6 +4,7 @@
"NETWORK": "__MEMPOOL_NETWORK__",
"BACKEND": "__MEMPOOL_BACKEND__",
"BLOCKS_SUMMARIES_INDEXING": true,
"GOGGLES_INDEXING": false,
"HTTP_PORT": 1,
"SPAWN_CLUSTER_PROCS": 2,
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
@@ -34,7 +35,8 @@
"DISK_CACHE_BLOCK_INTERVAL": 999,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true,
"PRICE_UPDATES_PER_HOUR": 1
"PRICE_UPDATES_PER_HOUR": 1,
"MAX_TRACKED_ADDRESSES": 1
},
"CORE_RPC": {
"HOST": "__CORE_RPC_HOST__",

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

@@ -17,6 +17,7 @@ describe('Mempool Backend Config', () => {
NETWORK: 'mainnet',
BACKEND: 'none',
BLOCKS_SUMMARIES_INDEXING: false,
GOGGLES_INDEXING: false,
HTTP_PORT: 8999,
SPAWN_CLUSTER_PROCS: 0,
API_URL_PREFIX: '/api/v1/',
@@ -48,6 +49,7 @@ describe('Mempool Backend Config', () => {
MAX_PUSH_TX_SIZE_WEIGHT: 400000,
ALLOW_UNREACHABLE: true,
PRICE_UPDATES_PER_HOUR: 1,
MAX_TRACKED_ADDRESSES: 1,
});
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });

View File

@@ -646,7 +646,7 @@ class BisqMarketsApi {
case 'year':
return strtotime('midnight first day of january', ts);
default:
throw new Error('Unsupported interval: ' + interval);
throw new Error('Unsupported interval');
}
}

View File

@@ -106,6 +106,7 @@ export namespace IBitcoinApi {
address?: string; // (string) bitcoin address
addresses?: string[]; // (string) bitcoin addresses
pegout_chain?: string; // (string) Elements peg-out chain
pegout_address?: string; // (string) Elements peg-out address
pegout_addresses?: string[]; // (string) Elements peg-out addresses
};
}

View File

@@ -4,6 +4,7 @@ import http from 'http';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger';
import { Common } from '../common';
interface FailoverHost {
host: string,
@@ -15,11 +16,13 @@ interface FailoverHost {
outOfSync?: boolean,
unreachable?: boolean,
preferred?: boolean,
checked: boolean,
}
class FailoverRouter {
activeHost: FailoverHost;
fallbackHost: FailoverHost;
maxHeight: number = 0;
hosts: FailoverHost[];
multihost: boolean;
pollInterval: number = 60000;
@@ -34,6 +37,7 @@ class FailoverRouter {
this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => {
return {
host: domain,
checked: false,
rtts: [],
rtt: Infinity,
failures: 0,
@@ -46,6 +50,7 @@ class FailoverRouter {
failures: 0,
socket: !!config.ESPLORA.UNIX_SOCKET_PATH,
preferred: true,
checked: false,
};
this.fallbackHost = this.activeHost;
this.hosts.unshift(this.activeHost);
@@ -74,66 +79,87 @@ class FailoverRouter {
clearTimeout(this.pollTimer);
}
const results = await Promise.allSettled(this.hosts.map(async (host) => {
if (host.socket) {
return this.pollConnection.get<number>('/blocks/tip/height', { socketPath: host.host, timeout: config.ESPLORA.FALLBACK_TIMEOUT });
} else {
return this.pollConnection.get<number>(host.host + '/blocks/tip/height', { timeout: config.ESPLORA.FALLBACK_TIMEOUT });
}
}));
const maxHeight = results.reduce((max, result) => Math.max(max, result.status === 'fulfilled' ? result.value?.data || 0 : 0), 0);
const start = Date.now();
// update rtts & sync status
for (let i = 0; i < results.length; i++) {
const host = this.hosts[i];
const result = results[i].status === 'fulfilled' ? (results[i] as PromiseFulfilledResult<AxiosResponse<number, any>>).value : null;
if (result) {
const height = result.data;
const rtt = result.config['meta'].rtt;
host.rtts.unshift(rtt);
host.rtts.slice(0, 5);
host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length;
host.latestHeight = height;
if (height == null || isNaN(height) || (maxHeight - height > 2)) {
host.outOfSync = true;
for (const host of this.hosts) {
try {
const result = await (host.socket
? this.pollConnection.get<number>('/blocks/tip/height', { socketPath: host.host, timeout: config.ESPLORA.FALLBACK_TIMEOUT })
: this.pollConnection.get<number>(host.host + '/blocks/tip/height', { timeout: config.ESPLORA.FALLBACK_TIMEOUT })
);
if (result) {
const height = result.data;
this.maxHeight = Math.max(height, this.maxHeight);
const rtt = result.config['meta'].rtt;
host.rtts.unshift(rtt);
host.rtts.slice(0, 5);
host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length;
host.latestHeight = height;
if (height == null || isNaN(height) || (this.maxHeight - height > 2)) {
host.outOfSync = true;
} else {
host.outOfSync = false;
}
host.unreachable = false;
} else {
host.outOfSync = false;
host.outOfSync = true;
host.unreachable = true;
host.rtts = [];
host.rtt = Infinity;
}
host.unreachable = false;
} else {
} catch (e) {
host.outOfSync = true;
host.unreachable = true;
host.rtts = [];
host.rtt = Infinity;
}
host.checked = true;
// switch if the current host is out of sync or significantly slower than the next best alternative
const rankOrder = this.sortHosts();
// switch if the current host is out of sync or significantly slower than the next best alternative
if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== rankOrder[0] && rankOrder[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (rankOrder[0].rtt * 2) + 50)) {
if (this.activeHost.unreachable) {
logger.warn(`🚨🚨🚨 Unable to reach ${this.activeHost.host}, failing over to next best alternative 🚨🚨🚨`);
} else if (this.activeHost.outOfSync) {
logger.warn(`🚨🚨🚨 ${this.activeHost.host} has fallen behind, failing over to next best alternative 🚨🚨🚨`);
} else {
logger.debug(`🛠️ ${this.activeHost.host} is no longer the best esplora host 🛠️`);
}
this.electHost();
}
await Common.sleep$(50);
}
this.sortHosts();
const rankOrder = this.updateFallback();
logger.debug(`Tomahawk ranking:\n${rankOrder.map((host, index) => this.formatRanking(index, host, this.activeHost, this.maxHeight)).join('\n')}`);
logger.debug(`Tomahawk ranking:\n${this.hosts.map((host, index) => this.formatRanking(index, host, this.activeHost, maxHeight)).join('\n')}`);
const elapsed = Date.now() - start;
// switch if the current host is out of sync or significantly slower than the next best alternative
if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== this.hosts[0] && this.hosts[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (this.hosts[0].rtt * 2) + 50)) {
if (this.activeHost.unreachable) {
logger.warn(`🚨🚨🚨 Unable to reach ${this.activeHost.host}, failing over to next best alternative 🚨🚨🚨`);
} else if (this.activeHost.outOfSync) {
logger.warn(`🚨🚨🚨 ${this.activeHost.host} has fallen behind, failing over to next best alternative 🚨🚨🚨`);
} else {
logger.debug(`🛠️ ${this.activeHost.host} is no longer the best esplora host 🛠️`);
}
this.electHost();
}
this.pollTimer = setTimeout(() => { this.pollHosts(); }, this.pollInterval);
this.pollTimer = setTimeout(() => { this.pollHosts(); }, Math.max(1, this.pollInterval - elapsed));
}
private formatRanking(index: number, host: FailoverHost, active: FailoverHost, maxHeight: number): string {
const heightStatus = host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '✅');
return `${host === active ? '⭐️' : ' '} ${host.rtt < Infinity ? Math.round(host.rtt).toString().padStart(5, ' ') + 'ms' : ' - '} ${host.unreachable ? '🔥' : '✅'} | block: ${host.latestHeight || '??????'} ${heightStatus} | ${host.host} ${host === active ? '⭐️' : ' '}`;
const heightStatus = !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '✅'));
return `${host === active ? '⭐️' : ' '} ${host.rtt < Infinity ? Math.round(host.rtt).toString().padStart(5, ' ') + 'ms' : ' - '} ${!host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅')} | block: ${host.latestHeight || '??????'} ${heightStatus} | ${host.host} ${host === active ? '⭐️' : ' '}`;
}
private updateFallback(): FailoverHost[] {
const rankOrder = this.sortHosts();
if (rankOrder.length > 1 && rankOrder[0] === this.activeHost) {
this.fallbackHost = rankOrder[1];
} else {
this.fallbackHost = rankOrder[0];
}
return rankOrder;
}
// sort hosts by connection quality, and update default fallback
private sortHosts(): void {
private sortHosts(): FailoverHost[] {
// sort by connection quality
this.hosts.sort((a, b) => {
return this.hosts.slice().sort((a, b) => {
if ((a.unreachable || a.outOfSync) === (b.unreachable || b.outOfSync)) {
if (a.preferred === b.preferred) {
// lower rtt is best
@@ -145,19 +171,14 @@ class FailoverRouter {
return (a.unreachable || a.outOfSync) ? 1 : -1;
}
});
if (this.hosts.length > 1 && this.hosts[0] === this.activeHost) {
this.fallbackHost = this.hosts[1];
} else {
this.fallbackHost = this.hosts[0];
}
}
// depose the active host and choose the next best replacement
private electHost(): void {
this.activeHost.outOfSync = true;
this.activeHost.failures = 0;
this.sortHosts();
this.activeHost = this.hosts[0];
const rankOrder = this.sortHosts();
this.activeHost = rankOrder[0];
logger.warn(`Switching esplora host to ${this.activeHost.host}`);
}

View File

@@ -2,7 +2,7 @@ import config from '../config';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import logger from '../logger';
import memPool from './mempool';
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified } from '../mempool.interfaces';
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit } from '../mempool.interfaces';
import { Common } from './common';
import diskCache from './disk-cache';
import transactionUtils from './transaction-utils';
@@ -37,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;
@@ -451,7 +453,9 @@ class Blocks {
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
if (cpfpSummary) {
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
}
} else {
await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary
}
@@ -565,8 +569,13 @@ 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.blocksSummariesIndexingEnabled() || config.MEMPOOL.BACKEND !== 'esplora') {
if (!Common.gogglesIndexingEnabled() || config.MEMPOOL.BACKEND !== 'esplora') {
return;
}
@@ -606,60 +615,68 @@ class Blocks {
logger.debug(`Classifying blocks and templates from #${currentBlockHeight} to #${minHeight}`, logger.tags.goggles);
for (let height = currentBlockHeight; height >= 0; height--) {
let txs: TransactionExtended[] | null = null;
if (unclassifiedBlocks[height]) {
const blockHash = unclassifiedBlocks[height];
// fetch transactions
txs = (await bitcoinApi.$getTxsForBlock(blockHash)).map(tx => transactionUtils.extendTransaction(tx));
// add CPFP
const cpfpSummary = Common.calculateCpfp(height, txs, true);
// classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 1);
}
if (unclassifiedTemplates[height]) {
// classify template
const blockHash = unclassifiedTemplates[height];
const template = await BlocksSummariesRepository.$getTemplate(blockHash);
const alreadyClassified = template?.transactions.reduce((classified, tx) => (classified || tx.flags > 0), false);
let classifiedTemplate = template?.transactions || [];
if (!alreadyClassified) {
const templateTxs: (TransactionExtended | TransactionClassified)[] = [];
const blockTxMap: { [txid: string]: TransactionExtended } = {};
for (const tx of (txs || [])) {
blockTxMap[tx.txid] = tx;
}
for (const templateTx of (template?.transactions || [])) {
let tx: TransactionExtended | null = blockTxMap[templateTx.txid];
if (!tx) {
try {
tx = await transactionUtils.$getTransactionExtended(templateTx.txid, false, true, false);
} catch (e) {
// transaction probably not found
}
}
templateTxs.push(tx || templateTx);
}
const cpfpSummary = Common.calculateCpfp(height, txs?.filter(tx => tx.effectiveFeePerVsize != null) as TransactionExtended[], true);
try {
let txs: TransactionExtended[] | null = null;
if (unclassifiedBlocks[height]) {
const blockHash = unclassifiedBlocks[height];
// fetch transactions
txs = (await bitcoinApi.$getTxsForBlock(blockHash)).map(tx => transactionUtils.extendTransaction(tx)) || [];
// add CPFP
const cpfpSummary = Common.calculateCpfp(height, txs, true);
// classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
for (const tx of classifiedTxs) {
classifiedTxMap[tx.txid] = tx;
}
classifiedTemplate = classifiedTemplate.map(tx => {
if (classifiedTxMap[tx.txid]) {
tx.flags = classifiedTxMap[tx.txid].flags || 0;
}
return tx;
});
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 1);
await Common.sleep$(250);
}
BlocksSummariesRepository.$saveTemplate({ height, template: { id: blockHash, transactions: classifiedTemplate }, version: 1 });
if (unclassifiedTemplates[height]) {
// classify template
const blockHash = unclassifiedTemplates[height];
const template = await BlocksSummariesRepository.$getTemplate(blockHash);
const alreadyClassified = template?.transactions?.reduce((classified, tx) => (classified || tx.flags > 0), false);
let classifiedTemplate = template?.transactions || [];
if (!alreadyClassified) {
const templateTxs: (TransactionExtended | TransactionClassified)[] = [];
const blockTxMap: { [txid: string]: TransactionExtended } = {};
for (const tx of (txs || [])) {
blockTxMap[tx.txid] = tx;
}
for (const templateTx of (template?.transactions || [])) {
let tx: TransactionExtended | null = blockTxMap[templateTx.txid];
if (!tx) {
try {
tx = await transactionUtils.$getTransactionExtended(templateTx.txid, false, true, false);
} catch (e) {
// transaction probably not found
}
}
templateTxs.push(tx || templateTx);
}
const cpfpSummary = Common.calculateCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as TransactionExtended[], true);
// classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
for (const tx of classifiedTxs) {
classifiedTxMap[tx.txid] = tx;
}
classifiedTemplate = classifiedTemplate.map(tx => {
if (classifiedTxMap[tx.txid]) {
tx.flags = classifiedTxMap[tx.txid].flags || 0;
}
return tx;
});
}
await BlocksSummariesRepository.$saveTemplate({ height, template: { id: blockHash, transactions: classifiedTemplate }, version: 1 });
await Common.sleep$(250);
}
} catch (e) {
logger.warn(`Failed to classify template or block summary at ${height}`, logger.tags.goggles);
}
// timing & logging
indexedThisRun++;
indexedTotal++;
if (unclassifiedBlocks[height] || unclassifiedTemplates[height]) {
indexedThisRun++;
indexedTotal++;
}
const elapsedSeconds = (Date.now() - timer) / 1000;
if (elapsedSeconds > 5) {
const perSecond = indexedThisRun / elapsedSeconds;
@@ -668,6 +685,8 @@ class Blocks {
indexedThisRun = 0;
}
}
this.classifyingBlocks = false;
}
/**
@@ -765,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`);
@@ -987,11 +1016,11 @@ class Blocks {
return state;
}
private updateTimerProgress(state, msg) {
private updateTimerProgress(state, msg): void {
state.progress = msg;
}
private clearTimer(state) {
private clearTimer(state): void {
if (state.timer) {
clearTimeout(state.timer);
}
@@ -1080,13 +1109,19 @@ class Blocks {
summary = {
id: hash,
transactions: cpfpSummary.transactions.map(tx => {
let flags: number = 0;
try {
flags = tx.flags || Common.getTransactionFlags(tx);
} catch (e) {
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
}
return {
txid: tx.txid,
fee: tx.fee || 0,
vsize: tx.vsize,
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)),
rate: tx.effectiveFeePerVsize,
flags: tx.flags || Common.getTransactionFlags(tx),
flags: flags,
};
}),
};
@@ -1276,7 +1311,7 @@ class Blocks {
return blocks;
}
public async $getBlockAuditSummary(hash: string): Promise<any> {
public async $getBlockAuditSummary(hash: string): Promise<BlockAudit | null> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
return BlocksAuditsRepository.$getBlockAudit(hash);
} else {
@@ -1292,11 +1327,15 @@ class Blocks {
return this.previousDifficultyRetarget;
}
public getQuarterEpochBlockTime(): number | null {
return this.quarterEpochBlockTime;
}
public getCurrentBlockHeight(): number {
return this.currentBlockHeight;
}
public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary> {
public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary | null> {
let transactions = txs;
if (!transactions) {
if (config.MEMPOOL.BACKEND === 'esplora') {
@@ -1311,14 +1350,19 @@ class Blocks {
}
}
const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]);
if (transactions?.length != null) {
const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]);
await this.$saveCpfp(hash, height, summary);
await this.$saveCpfp(hash, height, summary);
const effectiveFeeStats = Common.calcEffectiveFeeStatistics(summary.transactions);
await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats);
const effectiveFeeStats = Common.calcEffectiveFeeStatistics(summary.transactions);
await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats);
return summary;
return summary;
} else {
logger.err(`Cannot index CPFP for block ${height} - missing transaction data`);
return null;
}
}
public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> {

View File

@@ -6,6 +6,7 @@ import { NodeSocket } from '../repositories/NodesSocketsRepository';
import { isIP } from 'net';
import transactionUtils from './transaction-utils';
import { isPoint } from '../utils/secp256k1';
import logger from '../logger';
export class Common {
static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ?
'144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49'
@@ -244,8 +245,11 @@ export class Common {
flags |= TransactionFlags.v1;
} else if (tx.version === 2) {
flags |= TransactionFlags.v2;
} else if (tx.version === 3) {
flags |= TransactionFlags.v3;
}
const reusedAddresses: { [address: string ]: number } = {};
const reusedInputAddresses: { [address: string ]: number } = {};
const reusedOutputAddresses: { [address: string ]: number } = {};
const inValues = {};
const outValues = {};
let rbf = false;
@@ -261,6 +265,9 @@ export class Common {
case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break;
case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
case 'v1_p2tr': {
if (!vin.witness?.length) {
throw new Error('Taproot input missing witness data');
}
flags |= TransactionFlags.p2tr;
// in taproot, if the last witness item begins with 0x50, it's an annex
const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50');
@@ -286,7 +293,7 @@ export class Common {
}
if (vin.prevout?.scriptpubkey_address) {
reusedAddresses[vin.prevout?.scriptpubkey_address] = (reusedAddresses[vin.prevout?.scriptpubkey_address] || 0) + 1;
reusedInputAddresses[vin.prevout?.scriptpubkey_address] = (reusedInputAddresses[vin.prevout?.scriptpubkey_address] || 0) + 1;
}
inValues[vin.prevout?.value || Math.random()] = (inValues[vin.prevout?.value || Math.random()] || 0) + 1;
}
@@ -301,7 +308,7 @@ export class Common {
case 'p2pk': {
flags |= TransactionFlags.p2pk;
// detect fake pubkey (i.e. not a valid DER point on the secp256k1 curve)
hasFakePubkey = hasFakePubkey || !isPoint(vout.scriptpubkey.slice(2, -2));
hasFakePubkey = hasFakePubkey || !isPoint(vout.scriptpubkey?.slice(2, -2));
} break;
case 'multisig': {
flags |= TransactionFlags.p2ms;
@@ -321,7 +328,7 @@ export class Common {
case 'op_return': flags |= TransactionFlags.op_return; break;
}
if (vout.scriptpubkey_address) {
reusedAddresses[vout.scriptpubkey_address] = (reusedAddresses[vout.scriptpubkey_address] || 0) + 1;
reusedOutputAddresses[vout.scriptpubkey_address] = (reusedOutputAddresses[vout.scriptpubkey_address] || 0) + 1;
}
outValues[vout.value || Math.random()] = (outValues[vout.value || Math.random()] || 0) + 1;
}
@@ -331,7 +338,7 @@ export class Common {
// fast but bad heuristic to detect possible coinjoins
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
const addressReuse = Object.values(reusedAddresses).reduce((acc, count) => Math.max(acc, count), 0) > 1;
const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
if (!addressReuse && tx.vin.length >= 5 && tx.vout.length >= 5 && (Object.keys(inValues).length + Object.keys(outValues).length) <= (tx.vin.length + tx.vout.length) / 2 ) {
flags |= TransactionFlags.coinjoin;
}
@@ -348,7 +355,12 @@ export class Common {
}
static classifyTransaction(tx: TransactionExtended): TransactionClassified {
const flags = Common.getTransactionFlags(tx);
let flags = 0;
try {
flags = Common.getTransactionFlags(tx);
} catch (e) {
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
}
tx.flags = flags;
return {
...Common.stripTransaction(tx),
@@ -508,6 +520,13 @@ export class Common {
);
}
static gogglesIndexingEnabled(): boolean {
return (
Common.blocksSummariesIndexingEnabled() &&
config.MEMPOOL.GOGGLES_INDEXING === true
);
}
static cpfpIndexingEnabled(): boolean {
return (
Common.indexingEnabled() &&

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 67;
private static currentVersion = 68;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -566,6 +566,20 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `blocks_templates` ADD INDEX `version` (`version`)');
await this.updateToSchemaVersion(67);
}
if (databaseSchemaVersion < 68 && config.MEMPOOL.NETWORK === "liquid") {
await this.$executeQuery('TRUNCATE TABLE elements_pegs');
await this.$executeQuery('ALTER TABLE elements_pegs ADD PRIMARY KEY (txid, txindex);');
await this.$executeQuery(`UPDATE state SET number = 0 WHERE name = 'last_elements_block';`);
// Create the federation_addresses table and add the two Liquid Federation change addresses in
await this.$executeQuery(this.getCreateFederationAddressesTableQuery(), await this.$checkIfTableExists('federation_addresses'));
await this.$executeQuery(`INSERT INTO federation_addresses (bitcoinaddress) VALUES ('bc1qxvay4an52gcghxq5lavact7r6qe9l4laedsazz8fj2ee2cy47tlqff4aj4')`); // Federation change address
await this.$executeQuery(`INSERT INTO federation_addresses (bitcoinaddress) VALUES ('3EiAcrzq1cELXScc98KeCswGWZaPGceT1d')`); // Federation change address
// Create the federation_txos table that uses the federation_addresses table as a foreign key
await this.$executeQuery(this.getCreateFederationTxosTableQuery(), await this.$checkIfTableExists('federation_txos'));
await this.$executeQuery(`INSERT INTO state VALUES('last_bitcoin_block_audit', 0, NULL);`);
await this.updateToSchemaVersion(68);
}
}
/**
@@ -813,6 +827,32 @@ class DatabaseMigration {
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreateFederationAddressesTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS federation_addresses (
bitcoinaddress varchar(100) NOT NULL,
PRIMARY KEY (bitcoinaddress)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreateFederationTxosTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS federation_txos (
txid varchar(65) NOT NULL,
txindex int(11) NOT NULL,
bitcoinaddress varchar(100) NOT NULL,
amount bigint(20) unsigned NOT NULL,
blocknumber int(11) unsigned NOT NULL,
blocktime int(11) unsigned NOT NULL,
unspent tinyint(1) NOT NULL,
lastblockupdate int(11) unsigned NOT NULL,
lasttimeupdate int(11) unsigned NOT NULL,
pegtxid varchar(65) NOT NULL,
pegindex int(11) NOT NULL,
pegblocktime int(11) unsigned NOT NULL,
PRIMARY KEY (txid, txindex),
FOREIGN KEY (bitcoinaddress) REFERENCES federation_addresses (bitcoinaddress)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreatePoolsTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS pools (
id int(11) NOT NULL AUTO_INCREMENT,

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

@@ -81,7 +81,7 @@ class ChannelsApi {
public async $searchChannelsById(search: string): Promise<any[]> {
try {
// restrict search to valid id/short_id prefix formats
let searchStripped = search.match(/[0-9]+[0-9x]*/)?.[0] || '';
let searchStripped = search.match(/^[0-9]+[0-9x]*$/)?.[0] || '';
if (!searchStripped.length) {
return [];
}

View File

@@ -5,8 +5,12 @@ import { Common } from '../common';
import DB from '../../database';
import logger from '../../logger';
const federationChangeAddresses = ['bc1qxvay4an52gcghxq5lavact7r6qe9l4laedsazz8fj2ee2cy47tlqff4aj4', '3EiAcrzq1cELXScc98KeCswGWZaPGceT1d'];
const auditBlockOffsetWithTip = 1; // Wait for 1 block confirmation before processing the block in the audit process to reduce the risk of reorgs
class ElementsParser {
private isRunning = false;
private isUtxosUpdatingRunning = false;
constructor() { }
@@ -32,12 +36,6 @@ class ElementsParser {
}
}
public async $getPegDataByMonth(): Promise<any> {
const query = `SELECT SUM(amount) AS amount, DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y-%m-01') AS date FROM elements_pegs GROUP BY DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y%m')`;
const [rows] = await DB.query(query);
return rows;
}
protected async $parseBlock(block: IBitcoinApi.Block) {
for (const tx of block.tx) {
await this.$parseInputs(tx, block);
@@ -55,29 +53,30 @@ class ElementsParser {
protected async $parsePegIn(input: IBitcoinApi.Vin, vindex: number, txid: string, block: IBitcoinApi.Block) {
const bitcoinTx: IBitcoinApi.Transaction = await bitcoinSecondClient.getRawTransaction(input.txid, true);
const bitcoinBlock: IBitcoinApi.Block = await bitcoinSecondClient.getBlock(bitcoinTx.blockhash);
const prevout = bitcoinTx.vout[input.vout || 0];
const outputAddress = prevout.scriptPubKey.address || (prevout.scriptPubKey.addresses && prevout.scriptPubKey.addresses[0]) || '';
await this.$savePegToDatabase(block.height, block.time, prevout.value * 100000000, txid, vindex,
outputAddress, bitcoinTx.txid, prevout.n, 1);
outputAddress, bitcoinTx.txid, prevout.n, bitcoinBlock.height, bitcoinBlock.time, 1);
}
protected async $parseOutputs(tx: IBitcoinApi.Transaction, block: IBitcoinApi.Block) {
for (const output of tx.vout) {
if (output.scriptPubKey.pegout_chain) {
await this.$savePegToDatabase(block.height, block.time, 0 - output.value * 100000000, tx.txid, output.n,
(output.scriptPubKey.pegout_addresses && output.scriptPubKey.pegout_addresses[0] || ''), '', 0, 0);
(output.scriptPubKey.pegout_address || ''), '', 0, 0, 0, 0);
}
if (!output.scriptPubKey.pegout_chain && output.scriptPubKey.type === 'nulldata'
&& output.value && output.value > 0 && output.asset && output.asset === Common.nativeAssetId) {
await this.$savePegToDatabase(block.height, block.time, 0 - output.value * 100000000, tx.txid, output.n,
(output.scriptPubKey.pegout_addresses && output.scriptPubKey.pegout_addresses[0] || ''), '', 0, 1);
(output.scriptPubKey.pegout_address || ''), '', 0, 0, 0, 1);
}
}
}
protected async $savePegToDatabase(height: number, blockTime: number, amount: number, txid: string,
txindex: number, bitcoinaddress: string, bitcointxid: string, bitcoinindex: number, final_tx: number): Promise<void> {
const query = `INSERT INTO elements_pegs(
txindex: number, bitcoinaddress: string, bitcointxid: string, bitcoinindex: number, bitcoinblock: number, bitcoinBlockTime: number, final_tx: number): Promise<void> {
const query = `INSERT IGNORE INTO elements_pegs(
block, datetime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
@@ -85,7 +84,22 @@ class ElementsParser {
height, blockTime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx
];
await DB.query(query, params);
logger.debug(`Saved L-BTC peg from block height #${height} with TXID ${txid}.`);
logger.debug(`Saved L-BTC peg from Liquid block height #${height} with TXID ${txid}.`);
if (amount > 0) { // Peg-in
// Add the address to the federation addresses table
await DB.query(`INSERT IGNORE INTO federation_addresses (bitcoinaddress) VALUES (?)`, [bitcoinaddress]);
// Add the UTXO to the federation txos table
const query_utxos = `INSERT IGNORE INTO federation_txos (txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, unspent, lastblockupdate, lasttimeupdate, pegtxid, pegindex, pegblocktime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
const params_utxos: (string | number)[] = [bitcointxid, bitcoinindex, bitcoinaddress, amount, bitcoinblock, bitcoinBlockTime, 1, bitcoinblock - 1, 0, txid, txindex, blockTime];
await DB.query(query_utxos, params_utxos);
const [minBlockUpdate] = await DB.query(`SELECT MIN(lastblockupdate) AS lastblockupdate FROM federation_txos WHERE unspent = 1`)
await this.$saveLastBlockAuditToDatabase(minBlockUpdate[0]['lastblockupdate']);
logger.debug(`Saved new Federation UTXO ${bitcointxid}:${bitcoinindex} belonging to ${bitcoinaddress} to federation txos`);
}
}
protected async $getLatestBlockHeightFromDatabase(): Promise<number> {
@@ -98,6 +112,328 @@ class ElementsParser {
const query = `UPDATE state SET number = ? WHERE name = 'last_elements_block'`;
await DB.query(query, [blockHeight]);
}
///////////// FEDERATION AUDIT //////////////
public async $updateFederationUtxos() {
if (this.isUtxosUpdatingRunning) {
return;
}
this.isUtxosUpdatingRunning = true;
try {
let auditProgress = await this.$getAuditProgress();
// If no peg in transaction was found in the database, return
if (!auditProgress.lastBlockAudit) {
logger.debug(`No Federation UTXOs found in the database. Waiting for some to be confirmed before starting the Federation UTXOs audit`);
this.isUtxosUpdatingRunning = false;
return;
}
const bitcoinBlocksToSync = await this.$getBitcoinBlockchainState();
// If the bitcoin blockchain is not synced yet, return
if (bitcoinBlocksToSync.bitcoinHeaders > bitcoinBlocksToSync.bitcoinBlocks + 1) {
logger.debug(`Bitcoin client is not synced yet. ${bitcoinBlocksToSync.bitcoinHeaders - bitcoinBlocksToSync.bitcoinBlocks} blocks remaining to sync before the Federation audit process can start`);
this.isUtxosUpdatingRunning = false;
return;
}
auditProgress.lastBlockAudit++;
// Logging
let indexedThisRun = 0;
let timer = Date.now() / 1000;
const startedAt = Date.now() / 1000;
const indexingSpeeds: number[] = [];
while (auditProgress.lastBlockAudit <= auditProgress.confirmedTip) {
// First, get the current UTXOs that need to be scanned in the block
const utxos = await this.$getFederationUtxosToScan(auditProgress.lastBlockAudit);
// Get the peg-out addresses that need to be scanned
const redeemAddresses = await this.$getRedeemAddressesToScan();
// The fast way: check if these UTXOs are still unspent as of the current block with gettxout
let spentAsTip: any[];
let unspentAsTip: any[];
if (auditProgress.confirmedTip - auditProgress.lastBlockAudit <= 150) { // If the audit status is not too far in the past, we can use gettxout (fast way)
const utxosToParse = await this.$getFederationUtxosToParse(utxos);
spentAsTip = utxosToParse.spentAsTip;
unspentAsTip = utxosToParse.unspentAsTip;
logger.debug(`Found ${utxos.length} Federation UTXOs and ${redeemAddresses.length} Peg-Out Addresses to scan in Bitcoin block height #${auditProgress.lastBlockAudit} / #${auditProgress.confirmedTip}`);
logger.debug(`${unspentAsTip.length} / ${utxos.length} Federation UTXOs are unspent as of tip`);
} else { // If the audit status is too far in the past, it is useless and wasteful to look for still unspent txos since they will all be spent as of the tip
spentAsTip = utxos;
unspentAsTip = [];
// Logging
const elapsedSeconds = (Date.now() / 1000) - timer;
if (elapsedSeconds > 5) {
const runningFor = (Date.now() / 1000) - startedAt;
const blockPerSeconds = indexedThisRun / elapsedSeconds;
indexingSpeeds.push(blockPerSeconds);
if (indexingSpeeds.length > 100) indexingSpeeds.shift(); // Keep the length of the up to 100 last indexing speeds
const meanIndexingSpeed = indexingSpeeds.reduce((a, b) => a + b, 0) / indexingSpeeds.length;
const eta = (auditProgress.confirmedTip - auditProgress.lastBlockAudit) / meanIndexingSpeed;
logger.debug(`Scanning ${utxos.length} Federation UTXOs and ${redeemAddresses.length} Peg-Out Addresses at Bitcoin block height #${auditProgress.lastBlockAudit} / #${auditProgress.confirmedTip} | ~${meanIndexingSpeed.toFixed(2)} blocks/sec | elapsed: ${(runningFor / 60).toFixed(0)} minutes | ETA: ${(eta / 60).toFixed(0)} minutes`);
timer = Date.now() / 1000;
indexedThisRun = 0;
}
}
// The slow way: parse the block to look for the spending tx
const blockHash: IBitcoinApi.ChainTips = await bitcoinSecondClient.getBlockHash(auditProgress.lastBlockAudit);
const block: IBitcoinApi.Block = await bitcoinSecondClient.getBlock(blockHash, 2);
await this.$parseBitcoinBlock(block, spentAsTip, unspentAsTip, auditProgress.confirmedTip, redeemAddresses);
// Finally, update the lastblockupdate of the remaining UTXOs and save to the database
const [minBlockUpdate] = await DB.query(`SELECT MIN(lastblockupdate) AS lastblockupdate FROM federation_txos WHERE unspent = 1`)
await this.$saveLastBlockAuditToDatabase(minBlockUpdate[0]['lastblockupdate']);
auditProgress = await this.$getAuditProgress();
auditProgress.lastBlockAudit++;
indexedThisRun++;
}
this.isUtxosUpdatingRunning = false;
} catch (e) {
this.isUtxosUpdatingRunning = false;
throw new Error(e instanceof Error ? e.message : 'Error');
}
}
// Get the UTXOs that need to be scanned in block height (UTXOs that were last updated in the block height - 1)
protected async $getFederationUtxosToScan(height: number) {
const query = `SELECT txid, txindex, bitcoinaddress, amount FROM federation_txos WHERE lastblockupdate = ? AND unspent = 1`;
const [rows] = await DB.query(query, [height - 1]);
return rows as any[];
}
// Returns the UTXOs that are spent as of tip and need to be scanned
protected async $getFederationUtxosToParse(utxos: any[]): Promise<any> {
const spentAsTip: any[] = [];
const unspentAsTip: any[] = [];
for (const utxo of utxos) {
const result = await bitcoinSecondClient.getTxOut(utxo.txid, utxo.txindex, false);
result ? unspentAsTip.push(utxo) : spentAsTip.push(utxo);
}
return {spentAsTip, unspentAsTip};
}
protected async $parseBitcoinBlock(block: IBitcoinApi.Block, spentAsTip: any[], unspentAsTip: any[], confirmedTip: number, redeemAddressesData: any[] = []) {
const redeemAddresses: string[] = redeemAddressesData.map(redeemAddress => redeemAddress.bitcoinaddress);
for (const tx of block.tx) {
let mightRedeemInThisTx = false; // If a Federation UTXO is spent in this block, we might find a peg-out address in the outputs...
// Check if the Federation UTXOs that was spent as of tip are spent in this block
for (const input of tx.vin) {
const txo = spentAsTip.find(txo => txo.txid === input.txid && txo.txindex === input.vout);
if (txo) {
mightRedeemInThisTx = true;
await DB.query(`UPDATE federation_txos SET unspent = 0, lastblockupdate = ?, lasttimeupdate = ? WHERE txid = ? AND txindex = ?`, [block.height, block.time, txo.txid, txo.txindex]);
// Remove the TXO from the utxo array
spentAsTip.splice(spentAsTip.indexOf(txo), 1);
logger.debug(`Federation UTXO ${txo.txid}:${txo.txindex} (${txo.amount} sats) was spent in block ${block.height}`);
}
}
// Check if an output is sent to a change address of the federation
for (const output of tx.vout) {
if (output.scriptPubKey.address && federationChangeAddresses.includes(output.scriptPubKey.address)) {
// Check that the UTXO was not already added in the DB by previous scans
const [rows_check] = await DB.query(`SELECT txid FROM federation_txos WHERE txid = ? AND txindex = ?`, [tx.txid, output.n]) as any[];
if (rows_check.length === 0) {
const query_utxos = `INSERT INTO federation_txos (txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, unspent, lastblockupdate, lasttimeupdate, pegtxid, pegindex, pegblocktime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
const params_utxos: (string | number)[] = [tx.txid, output.n, output.scriptPubKey.address, output.value * 100000000, block.height, block.time, 1, block.height, 0, '', 0, 0];
await DB.query(query_utxos, params_utxos);
// Add the UTXO to the utxo array
spentAsTip.push({
txid: tx.txid,
txindex: output.n,
bitcoinaddress: output.scriptPubKey.address,
amount: output.value * 100000000
});
logger.debug(`Added new Federation UTXO ${tx.txid}:${output.n} (${output.value * 100000000} sats), change address: ${output.scriptPubKey.address}`);
}
}
if (mightRedeemInThisTx && output.scriptPubKey.address && redeemAddresses.includes(output.scriptPubKey.address)) {
// Find the number of times output.scriptPubKey.address appears in redeemAddresses. There can be address reuse for peg-outs...
const matchingAddress: any[] = redeemAddressesData.filter(redeemAddress => redeemAddress.bitcoinaddress === output.scriptPubKey.address && -redeemAddress.amount === Math.round(output.value * 100000000));
if (matchingAddress.length > 0) {
if (matchingAddress.length > 1) {
// If there are more than one peg out address with the same amount, we can't know which one redeemed the UTXO: we take the oldest one
matchingAddress.sort((a, b) => a.datetime - b.datetime);
logger.debug(`Found redeem txid ${tx.txid}:${output.n} to peg-out address ${matchingAddress[0].bitcoinaddress}, amount ${matchingAddress[0].amount}, datetime ${matchingAddress[0].datetime}`);
} else {
logger.debug(`Found redeem txid ${tx.txid}:${output.n} to peg-out address ${matchingAddress[0].bitcoinaddress}, amount ${matchingAddress[0].amount}`);
}
const query_add_redeem = `UPDATE elements_pegs SET bitcointxid = ?, bitcoinindex = ? WHERE bitcoinaddress = ? AND amount = ? AND datetime = ?`;
const params_add_redeem: (string | number)[] = [tx.txid, output.n, matchingAddress[0].bitcoinaddress, matchingAddress[0].amount, matchingAddress[0].datetime];
await DB.query(query_add_redeem, params_add_redeem);
const index = redeemAddressesData.indexOf(matchingAddress[0]);
redeemAddressesData.splice(index, 1);
redeemAddresses.splice(index, 1);
} else { // The output amount does not match the peg-out amount... log it
logger.debug(`Found redeem txid ${tx.txid}:${output.n} to peg-out address ${output.scriptPubKey.address} but output amount ${Math.round(output.value * 100000000)} does not match the peg-out amount!`);
}
}
}
}
for (const utxo of spentAsTip) {
await DB.query(`UPDATE federation_txos SET lastblockupdate = ? WHERE txid = ? AND txindex = ?`, [block.height, utxo.txid, utxo.txindex]);
}
for (const utxo of unspentAsTip) {
await DB.query(`UPDATE federation_txos SET lastblockupdate = ? WHERE txid = ? AND txindex = ?`, [confirmedTip, utxo.txid, utxo.txindex]);
}
}
protected async $saveLastBlockAuditToDatabase(blockHeight: number) {
const query = `UPDATE state SET number = ? WHERE name = 'last_bitcoin_block_audit'`;
await DB.query(query, [blockHeight]);
}
// Get the bitcoin block where the audit process was last updated
protected async $getAuditProgress(): Promise<any> {
const lastblockaudit = await this.$getLastBlockAudit();
const bitcoinBlocksToSync = await this.$getBitcoinBlockchainState();
return {
lastBlockAudit: lastblockaudit,
confirmedTip: bitcoinBlocksToSync.bitcoinBlocks - auditBlockOffsetWithTip,
};
}
// Get the bitcoin blocks remaining to be synced
protected async $getBitcoinBlockchainState(): Promise<any> {
const result = await bitcoinSecondClient.getBlockchainInfo();
return {
bitcoinBlocks: result.blocks,
bitcoinHeaders: result.headers,
}
}
protected async $getLastBlockAudit(): Promise<number> {
const query = `SELECT number FROM state WHERE name = 'last_bitcoin_block_audit'`;
const [rows] = await DB.query(query);
return rows[0]['number'];
}
protected async $getRedeemAddressesToScan(): Promise<any[]> {
const query = `SELECT datetime, amount, bitcoinaddress FROM elements_pegs where amount < 0 AND bitcoinaddress != '' AND bitcointxid = '';`;
const [rows]: any[] = await DB.query(query);
return rows;
}
///////////// DATA QUERY //////////////
public async $getAuditStatus(): Promise<any> {
const lastBlockAudit = await this.$getLastBlockAudit();
const bitcoinBlocksToSync = await this.$getBitcoinBlockchainState();
return {
bitcoinBlocks: bitcoinBlocksToSync.bitcoinBlocks,
bitcoinHeaders: bitcoinBlocksToSync.bitcoinHeaders,
lastBlockAudit: lastBlockAudit,
isAuditSynced: bitcoinBlocksToSync.bitcoinHeaders - bitcoinBlocksToSync.bitcoinBlocks <= 2 && bitcoinBlocksToSync.bitcoinBlocks - lastBlockAudit <= 3,
};
}
public async $getPegDataByMonth(): Promise<any> {
const query = `SELECT SUM(amount) AS amount, DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y-%m-01') AS date FROM elements_pegs GROUP BY DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y%m')`;
const [rows] = await DB.query(query);
return rows;
}
public async $getFederationReservesByMonth(): Promise<any> {
const query = `
SELECT SUM(amount) AS amount, DATE_FORMAT(FROM_UNIXTIME(blocktime), '%Y-%m-01') AS date FROM federation_txos
WHERE
(blocktime > UNIX_TIMESTAMP(LAST_DAY(FROM_UNIXTIME(blocktime) - INTERVAL 1 MONTH) + INTERVAL 1 DAY))
AND
((unspent = 1) OR (unspent = 0 AND lasttimeupdate > UNIX_TIMESTAMP(LAST_DAY(FROM_UNIXTIME(blocktime)) + INTERVAL 1 DAY)))
GROUP BY
date;`;
const [rows] = await DB.query(query);
return rows;
}
// Get the current L-BTC pegs and the last Liquid block it was updated
public async $getCurrentLbtcSupply(): Promise<any> {
const [rows] = await DB.query(`SELECT SUM(amount) AS LBTC_supply FROM elements_pegs;`);
const lastblockupdate = await this.$getLatestBlockHeightFromDatabase();
const hash = await bitcoinClient.getBlockHash(lastblockupdate);
return {
amount: rows[0]['LBTC_supply'],
lastBlockUpdate: lastblockupdate,
hash: hash
};
}
// Get the current reserves of the federation and the last Bitcoin block it was updated
public async $getCurrentFederationReserves(): Promise<any> {
const [rows] = await DB.query(`SELECT SUM(amount) AS total_balance FROM federation_txos WHERE unspent = 1;`);
const lastblockaudit = await this.$getLastBlockAudit();
const hash = await bitcoinSecondClient.getBlockHash(lastblockaudit);
return {
amount: rows[0]['total_balance'],
lastBlockUpdate: lastblockaudit,
hash: hash
};
}
// Get all of the federation addresses, most balances first
public async $getFederationAddresses(): Promise<any> {
const query = `SELECT bitcoinaddress, SUM(amount) AS balance FROM federation_txos WHERE unspent = 1 GROUP BY bitcoinaddress ORDER BY balance DESC;`;
const [rows] = await DB.query(query);
return rows;
}
// Get all of the UTXOs held by the federation, most recent first
public async $getFederationUtxos(): Promise<any> {
const query = `SELECT txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, pegtxid, pegindex, pegblocktime FROM federation_txos WHERE unspent = 1 ORDER BY blocktime DESC;`;
const [rows] = await DB.query(query);
return rows;
}
// 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 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 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;
}
// Get all peg in / out from the last month
public async $getPegsVolumeDaily(): Promise<any> {
const pegInQuery = await DB.query(`SELECT SUM(amount) AS volume, COUNT(*) AS number FROM elements_pegs WHERE amount > 0 and datetime > UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -1, CURRENT_TIMESTAMP()));`);
const pegOutQuery = await DB.query(`SELECT SUM(amount) AS volume, COUNT(*) AS number FROM elements_pegs WHERE amount < 0 and datetime > UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -1, CURRENT_TIMESTAMP()));`);
return [
pegInQuery[0][0],
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

@@ -15,7 +15,18 @@ class LiquidRoutes {
if (config.DATABASE.ENABLED) {
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/reserves/addresses', this.$getFederationAddresses)
.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/total', this.$getFederationUtxosNumber)
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/status', this.$getFederationAuditStatus)
;
}
}
@@ -63,11 +74,147 @@ class LiquidRoutes {
private async $getElementsPegsByMonth(req: Request, res: Response) {
try {
const pegs = await elementsParser.$getPegDataByMonth();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(pegs);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getFederationReservesByMonth(req: Request, res: Response) {
try {
const reserves = await elementsParser.$getFederationReservesByMonth();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(reserves);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getElementsPegs(req: Request, res: Response) {
try {
const currentSupply = await elementsParser.$getCurrentLbtcSupply();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentSupply);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getFederationReserves(req: Request, res: Response) {
try {
const currentReserves = await elementsParser.$getCurrentFederationReserves();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentReserves);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getFederationAuditStatus(req: Request, res: Response) {
try {
const auditStatus = await elementsParser.$getAuditStatus();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(auditStatus);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getFederationAddresses(req: Request, res: Response) {
try {
const federationAddresses = await elementsParser.$getFederationAddresses();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
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);
}
}
private async $getFederationAddressesNumber(req: Request, res: Response) {
try {
const federationAddresses = await elementsParser.$getFederationAddressesNumber();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
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);
}
}
private async $getFederationUtxos(req: Request, res: Response) {
try {
const federationUtxos = await elementsParser.$getFederationUtxos();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
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 $getFederationUtxosNumber(req: Request, res: Response) {
try {
const federationUtxos = await elementsParser.$getFederationUtxosNumber();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
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 $getPegsList(req: Request, res: Response) {
try {
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(recentPegs);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getPegsVolumeDaily(req: Request, res: Response) {
try {
const pegsVolume = await elementsParser.$getPegsVolumeDaily();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsVolume);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
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

@@ -1,6 +1,6 @@
import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction, ThreadAcceleration as RustThreadAcceleration } from 'rust-gbt';
import logger from '../logger';
import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, PoolTag, TransactionClassified } from '../mempool.interfaces';
import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, PoolTag, TransactionClassified, TransactionCompressed, MempoolDeltaChange } from '../mempool.interfaces';
import { Common, OnlineFeeStatsCalculator } from './common';
import config from '../config';
import { Worker } from 'worker_threads';
@@ -171,7 +171,7 @@ class MempoolBlocks {
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
let added: TransactionClassified[] = [];
let removed: string[] = [];
const changed: { txid: string, rate: number | undefined, acc: boolean | undefined }[] = [];
const changed: TransactionClassified[] = [];
if (mempoolBlocks[i] && !prevBlocks[i]) {
added = mempoolBlocks[i].transactions;
} else if (!mempoolBlocks[i] && prevBlocks[i]) {
@@ -194,14 +194,14 @@ class MempoolBlocks {
if (!prevIds[tx.txid]) {
added.push(tx);
} else if (tx.rate !== prevIds[tx.txid].rate || tx.acc !== prevIds[tx.txid].acc) {
changed.push({ txid: tx.txid, rate: tx.rate, acc: tx.acc });
changed.push(tx);
}
});
}
mempoolBlockDeltas.push({
added,
added: added.map(this.compressTx),
removed,
changed,
changed: changed.map(this.compressDeltaChange),
});
}
return mempoolBlockDeltas;
@@ -691,6 +691,38 @@ class MempoolBlocks {
});
return { blocks: convertedBlocks, blockWeights, rates: convertedRates, clusters: convertedClusters, overflow: convertedOverflow };
}
public compressTx(tx: TransactionClassified): TransactionCompressed {
if (tx.acc) {
return [
tx.txid,
tx.fee,
tx.vsize,
tx.value,
Math.round((tx.rate || (tx.fee / tx.vsize)) * 100) / 100,
tx.flags,
1
];
} else {
return [
tx.txid,
tx.fee,
tx.vsize,
tx.value,
Math.round((tx.rate || (tx.fee / tx.vsize)) * 100) / 100,
tx.flags,
];
}
}
public compressDeltaChange(tx: TransactionClassified): MempoolDeltaChange {
return [
tx.txid,
Math.round((tx.rate || (tx.fee / tx.vsize)) * 100) / 100,
tx.flags,
tx.acc ? 1 : 0,
];
}
}
export default new MempoolBlocks();

View File

@@ -142,7 +142,7 @@ class Mining {
public async $getPoolStat(slug: string): Promise<object> {
const pool = await PoolsRepository.$getPool(slug);
if (!pool) {
throw new Error('This mining pool does not exist ' + escape(slug));
throw new Error('This mining pool does not exist');
}
const blockCount: number = await BlocksRepository.$blockCount(pool.id);

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

@@ -285,7 +285,7 @@ class StatisticsApi {
public async $list2H(): Promise<OptimizedStatistic[]> {
try {
const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics ORDER BY statistics.added DESC LIMIT 120`;
const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL 2 HOUR) AND NOW() ORDER BY statistics.added DESC`;
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
} catch (e) {
@@ -296,7 +296,7 @@ class StatisticsApi {
public async $list24H(): Promise<OptimizedStatistic[]> {
try {
const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics ORDER BY statistics.added DESC LIMIT 1440`;
const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL 24 HOUR) AND NOW() ORDER BY statistics.added DESC`;
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
} catch (e) {

View File

@@ -6,6 +6,7 @@ import statisticsApi from './statistics-api';
class Statistics {
protected intervalTimer: NodeJS.Timer | undefined;
protected lastRun: number = 0;
protected newStatisticsEntryCallback: ((stats: OptimizedStatistic) => void) | undefined;
public setNewStatisticsEntryCallback(fn: (stats: OptimizedStatistic) => void) {
@@ -23,15 +24,21 @@ class Statistics {
setTimeout(() => {
this.runStatistics();
this.intervalTimer = setInterval(() => {
this.runStatistics();
this.runStatistics(true);
}, 1 * 60 * 1000);
}, difference);
}
private async runStatistics(): Promise<void> {
public async runStatistics(skipIfRecent = false): Promise<void> {
if (!memPool.isInSync()) {
return;
}
if (skipIfRecent && new Date().getTime() / 1000 - this.lastRun < 30) {
return;
}
this.lastRun = new Date().getTime() / 1000;
const currentMempool = memPool.getMempool();
const txPerSecond = memPool.getTxPerSecond();
const vBytesPerSecond = memPool.getVBytesPerSecond();

View File

@@ -23,6 +23,13 @@ import priceUpdater from '../tasks/price-updater';
import { ApiPrice } from '../repositories/PricesRepository';
import accelerationApi from './services/acceleration';
import mempool from './mempool';
import statistics from './statistics/statistics';
interface AddressTransactions {
mempool: MempoolTransactionExtended[],
confirmed: MempoolTransactionExtended[],
removed: MempoolTransactionExtended[],
}
// valid 'want' subscriptions
const wantable = [
@@ -195,24 +202,49 @@ class WebsocketHandler {
}
if (parsedMessage && parsedMessage['track-address']) {
if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64})$/
.test(parsedMessage['track-address'])) {
let matchedAddress = parsedMessage['track-address'];
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(parsedMessage['track-address'])) {
matchedAddress = matchedAddress.toLowerCase();
}
if (/^04[a-fA-F0-9]{128}$/.test(parsedMessage['track-address'])) {
client['track-address'] = '41' + matchedAddress + 'ac';
} else if (/^(02|03)[a-fA-F0-9]{64}$/.test(parsedMessage['track-address'])) {
client['track-address'] = '21' + matchedAddress + 'ac';
} else {
client['track-address'] = matchedAddress;
}
const validAddress = this.testAddress(parsedMessage['track-address']);
if (validAddress) {
client['track-address'] = validAddress;
} else {
client['track-address'] = null;
}
}
if (parsedMessage && parsedMessage['track-addresses'] && Array.isArray(parsedMessage['track-addresses'])) {
const addressMap: { [address: string]: string } = {};
for (const address of parsedMessage['track-addresses']) {
const validAddress = this.testAddress(address);
if (validAddress) {
addressMap[address] = validAddress;
}
}
if (Object.keys(addressMap).length > config.MEMPOOL.MAX_TRACKED_ADDRESSES) {
response['track-addresses-error'] = `"too many addresses requested, this connection supports tracking a maximum of ${config.MEMPOOL.MAX_TRACKED_ADDRESSES} addresses"`;
client['track-addresses'] = null;
} else if (Object.keys(addressMap).length > 0) {
client['track-addresses'] = addressMap;
} else {
client['track-addresses'] = null;
}
}
if (parsedMessage && parsedMessage['track-scriptpubkeys'] && Array.isArray(parsedMessage['track-scriptpubkeys'])) {
const spks: string[] = [];
for (const spk of parsedMessage['track-scriptpubkeys']) {
if (/^[a-fA-F0-9]+$/.test(spk)) {
spks.push(spk.toLowerCase());
}
}
if (spks.length > config.MEMPOOL.MAX_TRACKED_ADDRESSES) {
response['track-scriptpubkeys-error'] = `"too many scriptpubkeys requested, this connection supports tracking a maximum of ${config.MEMPOOL.MAX_TRACKED_ADDRESSES} scriptpubkeys"`;
client['track-scriptpubkeys'] = null;
} else if (spks.length) {
client['track-scriptpubkeys'] = spks;
} else {
client['track-scriptpubkeys'] = null;
}
}
if (parsedMessage && parsedMessage['track-asset']) {
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) {
client['track-asset'] = parsedMessage['track-asset'];
@@ -228,7 +260,7 @@ class WebsocketHandler {
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
response['projected-block-transactions'] = JSON.stringify({
index: index,
blockTransactions: mBlocksWithTransactions[index]?.transactions || [],
blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
});
} else {
client['track-mempool-block'] = null;
@@ -544,6 +576,50 @@ class WebsocketHandler {
}
}
if (client['track-addresses']) {
const addressMap: { [address: string]: AddressTransactions } = {};
for (const [address, key] of Object.entries(client['track-addresses'] || {})) {
const newTransactions = Array.from(addressCache[key as string]?.values() || []);
const removedTransactions = Array.from(removedAddressCache[key as string]?.values() || []);
// txs may be missing prevouts in non-esplora backends
// so fetch the full transactions now
const fullTransactions = (config.MEMPOOL.BACKEND !== 'esplora') ? await this.getFullTransactions(newTransactions) : newTransactions;
if (fullTransactions?.length) {
addressMap[address] = {
mempool: fullTransactions,
confirmed: [],
removed: removedTransactions,
};
}
}
if (Object.keys(addressMap).length > 0) {
response['multi-address-transactions'] = JSON.stringify(addressMap);
}
}
if (client['track-scriptpubkeys']) {
const spkMap: { [spk: string]: AddressTransactions } = {};
for (const spk of client['track-scriptpubkeys'] || []) {
const newTransactions = Array.from(addressCache[spk as string]?.values() || []);
const removedTransactions = Array.from(removedAddressCache[spk as string]?.values() || []);
// txs may be missing prevouts in non-esplora backends
// so fetch the full transactions now
const fullTransactions = (config.MEMPOOL.BACKEND !== 'esplora') ? await this.getFullTransactions(newTransactions) : newTransactions;
if (fullTransactions?.length) {
spkMap[spk] = {
mempool: fullTransactions,
confirmed: [],
removed: removedTransactions,
};
}
}
if (Object.keys(spkMap).length > 0) {
response['multi-scriptpubkey-transactions'] = JSON.stringify(spkMap);
}
}
if (client['track-asset']) {
const foundTransactions: TransactionExtended[] = [];
@@ -648,6 +724,7 @@ class WebsocketHandler {
}
this.printLogs();
await statistics.runStatistics();
const _memPool = memPool.getMempool();
@@ -844,6 +921,42 @@ class WebsocketHandler {
}
}
if (client['track-addresses']) {
const addressMap: { [address: string]: AddressTransactions } = {};
for (const [address, key] of Object.entries(client['track-addresses'] || {})) {
const fullTransactions = Array.from(addressCache[key as string]?.values() || []);
if (fullTransactions?.length) {
addressMap[address] = {
mempool: [],
confirmed: fullTransactions,
removed: [],
};
}
}
if (Object.keys(addressMap).length > 0) {
response['multi-address-transactions'] = JSON.stringify(addressMap);
}
}
if (client['track-scriptpubkeys']) {
const spkMap: { [spk: string]: AddressTransactions } = {};
for (const spk of client['track-scriptpubkeys'] || []) {
const fullTransactions = Array.from(addressCache[spk as string]?.values() || []);
if (fullTransactions?.length) {
spkMap[spk] = {
mempool: [],
confirmed: fullTransactions,
removed: [],
};
}
}
if (Object.keys(spkMap).length > 0) {
response['multi-scriptpubkey-transactions'] = JSON.stringify(spkMap);
}
}
if (client['track-asset']) {
const foundTransactions: TransactionExtended[] = [];
@@ -888,7 +1001,7 @@ class WebsocketHandler {
if (mBlockDeltas[index].added.length > (mBlocksWithTransactions[index]?.transactions.length / 2)) {
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-full-${index}`, {
index: index,
blockTransactions: mBlocksWithTransactions[index].transactions,
blockTransactions: mBlocksWithTransactions[index].transactions.map(mempoolBlocks.compressTx),
});
} else {
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-delta-${index}`, {
@@ -903,6 +1016,8 @@ class WebsocketHandler {
client.send(this.serializeResponse(response));
}
});
await statistics.runStatistics();
}
// takes a dictionary of JSON serialized values
@@ -913,6 +1028,28 @@ class WebsocketHandler {
+ '}';
}
// checks if an address conforms to a valid format
// returns the canonical form:
// - lowercase for bech32(m)
// - lowercase scriptpubkey for P2PK
// or false if invalid
private testAddress(address): string | false {
if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64})$/.test(address)) {
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(address)) {
address = address.toLowerCase();
}
if (/^04[a-fA-F0-9]{128}$/.test(address)) {
return '41' + address + 'ac';
} else if (/^(02|03)[a-fA-F0-9]{64}$/.test(address)) {
return '21' + address + 'ac';
} else {
return address;
}
} else {
return false;
}
}
private makeAddressCache(transactions: MempoolTransactionExtended[]): { [address: string]: Set<MempoolTransactionExtended> } {
const addressCache: { [address: string]: Set<MempoolTransactionExtended> } = {};
for (const tx of transactions) {

View File

@@ -20,6 +20,7 @@ interface IConfig {
MEMPOOL_BLOCKS_AMOUNT: number;
INDEXING_BLOCKS_AMOUNT: number;
BLOCKS_SUMMARIES_INDEXING: boolean;
GOGGLES_INDEXING: boolean;
USE_SECOND_NODE_FOR_MINFEE: boolean;
EXTERNAL_ASSETS: string[];
EXTERNAL_MAX_RETRY: number;
@@ -39,6 +40,7 @@ interface IConfig {
MAX_PUSH_TX_SIZE_WEIGHT: number;
ALLOW_UNREACHABLE: boolean;
PRICE_UPDATES_PER_HOUR: number;
MAX_TRACKED_ADDRESSES: number;
};
ESPLORA: {
REST_API_URL: string;
@@ -174,6 +176,7 @@ const defaults: IConfig = {
'MEMPOOL_BLOCKS_AMOUNT': 8,
'INDEXING_BLOCKS_AMOUNT': 11000, // 0 = disable indexing, -1 = index all blocks
'BLOCKS_SUMMARIES_INDEXING': false,
'GOGGLES_INDEXING': false,
'USE_SECOND_NODE_FOR_MINFEE': false,
'EXTERNAL_ASSETS': [],
'EXTERNAL_MAX_RETRY': 1,
@@ -193,6 +196,7 @@ const defaults: IConfig = {
'MAX_PUSH_TX_SIZE_WEIGHT': 400000,
'ALLOW_UNREACHABLE': true,
'PRICE_UPDATES_PER_HOUR': 1,
'MAX_TRACKED_ADDRESSES': 1,
},
'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000',

View File

@@ -266,6 +266,7 @@ class Server {
blocks.setNewBlockCallback(async () => {
try {
await elementsParser.$parse();
await elementsParser.$updateFederationUtxos();
} catch (e) {
logger.warn('Elements parsing error: ' + (e instanceof Error ? e.message : e));
}

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

@@ -65,9 +65,9 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
}
export interface MempoolBlockDelta {
added: TransactionClassified[];
added: TransactionCompressed[];
removed: string[];
changed: { txid: string, rate: number | undefined, flags?: number }[];
changed: MempoolDeltaChange[];
}
interface VinStrippedToScriptsig {
@@ -196,6 +196,11 @@ export interface TransactionClassified extends TransactionStripped {
flags: number;
}
// [txid, fee, vsize, value, rate, flags, acceleration?]
export type TransactionCompressed = [string, number, number, number, number, number, 1?];
// [txid, rate, flags, acceleration?]
export type MempoolDeltaChange = [string, number, number, (1|0)];
// binary flags for transaction classification
export const TransactionFlags = {
// features
@@ -203,6 +208,7 @@ export const TransactionFlags = {
no_rbf: 0b00000010n,
v1: 0b00000100n,
v2: 0b00001000n,
v3: 0b00010000n,
// address types
p2pk: 0b00000001_00000000n,
p2ms: 0b00000010_00000000n,
@@ -281,6 +287,7 @@ export interface BlockExtended extends IEsploraApi.Block {
export interface BlockSummary {
id: string;
transactions: TransactionClassified[];
version?: number;
}
export interface AuditSummary extends BlockAudit {

View File

@@ -59,7 +59,7 @@ class BlocksAuditRepositories {
}
}
public async $getBlockAudit(hash: string): Promise<any> {
public async $getBlockAudit(hash: string): Promise<BlockAudit | null> {
try {
const [rows]: any[] = await DB.query(
`SELECT blocks_audits.height, blocks_audits.hash as id, UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
@@ -75,8 +75,8 @@ class BlocksAuditRepositories {
expected_weight as expectedWeight
FROM blocks_audits
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
WHERE blocks_audits.hash = "${hash}"
`);
WHERE blocks_audits.hash = ?
`, [hash]);
if (rows.length) {
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
@@ -101,8 +101,8 @@ class BlocksAuditRepositories {
const [rows]: any[] = await DB.query(
`SELECT hash, match_rate as matchRate, expected_fees as expectedFees, expected_weight as expectedWeight
FROM blocks_audits
WHERE blocks_audits.hash = "${hash}"
`);
WHERE blocks_audits.hash = ?
`, [hash]);
return rows[0];
} catch (e: any) {
logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));

View File

@@ -5,7 +5,7 @@ import logger from '../logger';
import { Common } from '../api/common';
import PoolsRepository from './PoolsRepository';
import HashratesRepository from './HashratesRepository';
import { escape } from 'mysql2';
import { RowDataPacket, escape } from 'mysql2';
import BlocksSummariesRepository from './BlocksSummariesRepository';
import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository';
import bitcoinClient from '../api/bitcoin/bitcoin-client';
@@ -478,7 +478,7 @@ class BlocksRepository {
public async $getBlocksByPool(slug: string, startHeight?: number): Promise<BlockExtended[]> {
const pool = await PoolsRepository.$getPool(slug);
if (!pool) {
throw new Error('This mining pool does not exist ' + escape(slug));
throw new Error('This mining pool does not exist');
}
const params: any[] = [];
@@ -802,10 +802,10 @@ class BlocksRepository {
/**
* Get a list of blocks that have been indexed
*/
public async $getIndexedBlocks(): Promise<any[]> {
public async $getIndexedBlocks(): Promise<{ height: number, hash: string }[]> {
try {
const [rows]: any = await DB.query(`SELECT height, hash FROM blocks ORDER BY height DESC`);
return rows;
const [rows] = await DB.query(`SELECT height, hash FROM blocks ORDER BY height DESC`) as RowDataPacket[][];
return rows as { height: number, hash: string }[];
} catch (e) {
logger.err('Cannot generate block size and weight history. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
@@ -815,7 +815,7 @@ class BlocksRepository {
/**
* Get a list of blocks that have not had CPFP data indexed
*/
public async $getCPFPUnindexedBlocks(): Promise<any[]> {
public async $getCPFPUnindexedBlocks(): Promise<number[]> {
try {
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
const currentBlockHeight = blockchainInfo.blocks;
@@ -825,13 +825,13 @@ class BlocksRepository {
}
const minHeight = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
const [rows]: any[] = await DB.query(`
const [rows] = await DB.query(`
SELECT height
FROM compact_cpfp_clusters
WHERE height <= ? AND height >= ?
GROUP BY height
ORDER BY height DESC;
`, [currentBlockHeight, minHeight]);
`, [currentBlockHeight, minHeight]) as RowDataPacket[][];
const indexedHeights = {};
rows.forEach((row) => { indexedHeights[row.height] = true; });

View File

@@ -1,3 +1,4 @@
import { RowDataPacket } from 'mysql2';
import DB from '../database';
import logger from '../logger';
import { BlockSummary, TransactionClassified } from '../mempool.interfaces';
@@ -23,8 +24,8 @@ class BlocksSummariesRepository {
await DB.query(`
INSERT INTO blocks_summaries
SET height = ?, transactions = ?, id = ?, version = ?
ON DUPLICATE KEY UPDATE transactions = ?`,
[blockHeight, transactionsStr, blockId, version, transactionsStr]);
ON DUPLICATE KEY UPDATE transactions = ?, version = ?`,
[blockHeight, transactionsStr, blockId, version, transactionsStr, version]);
} catch (e: any) {
logger.debug(`Cannot save block summary transactions for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
@@ -39,8 +40,9 @@ class BlocksSummariesRepository {
INSERT INTO blocks_templates (id, template, version)
VALUE (?, ?, ?)
ON DUPLICATE KEY UPDATE
template = ?
`, [blockId, transactions, params.version, transactions]);
template = ?,
version = ?
`, [blockId, transactions, params.version, transactions, params.version]);
} catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Cannot save block template for ${blockId} because it has already been indexed, ignoring`);
@@ -57,6 +59,7 @@ class BlocksSummariesRepository {
return {
id: templates[0].id,
transactions: JSON.parse(templates[0].template),
version: templates[0].version,
};
}
} catch (e) {
@@ -67,7 +70,7 @@ class BlocksSummariesRepository {
public async $getIndexedSummariesId(): Promise<string[]> {
try {
const [rows]: any[] = await DB.query(`SELECT id from blocks_summaries`);
const [rows] = await DB.query(`SELECT id from blocks_summaries`) as RowDataPacket[][];
return rows.map(row => row.id);
} catch (e) {
logger.err(`Cannot get block summaries id list. Reason: ` + (e instanceof Error ? e.message : e));

View File

@@ -139,7 +139,7 @@ class HashratesRepository {
public async $getPoolWeeklyHashrate(slug: string): Promise<any[]> {
const pool = await PoolsRepository.$getPool(slug);
if (!pool) {
throw new Error('This mining pool does not exist ' + escape(slug));
throw new Error('This mining pool does not exist');
}
// Find hashrate boundaries

View File

@@ -31,6 +31,9 @@ const curveP = BigInt(`0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
* @returns {boolean} true if the point is on the SECP256K1 curve
*/
export function isPoint(pointHex: string): boolean {
if (!pointHex?.length) {
return false;
}
if (
!(
// is uncompressed

3
contributors/afahrer.txt Normal file
View File

@@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 23, 2024.
Signed: afahrer

View File

@@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of December 20, 2023.
Signed: jamesblacklock

View File

@@ -1,3 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of November 16, 2023.
Signed: natsee
Signed: natsoni

View File

@@ -22,6 +22,7 @@
"STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
"INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__,
"BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__,
"GOGGLES_INDEXING": __MEMPOOL_GOGGLES_INDEXING__,
"AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__,
"AUDIT": __MEMPOOL_AUDIT__,
"ADVANCED_GBT_AUDIT": __MEMPOOL_ADVANCED_GBT_AUDIT__,
@@ -34,7 +35,8 @@
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__
"PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__,
"MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__
},
"CORE_RPC": {
"HOST": "__CORE_RPC_HOST__",

View File

@@ -17,6 +17,7 @@ __MEMPOOL_INITIAL_BLOCKS_AMOUNT__=${MEMPOOL_INITIAL_BLOCKS_AMOUNT:=8}
__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_MEMPOOL_BLOCKS_AMOUNT:=8}
__MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=11000}
__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__=${MEMPOOL_BLOCKS_SUMMARIES_INDEXING:=false}
__MEMPOOL_GOGGLES_INDEXING__=${MEMPOOL_GOGGLES_INDEXING:=false}
__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__=${MEMPOOL_USE_SECOND_NODE_FOR_MINFEE:=false}
__MEMPOOL_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[]}
__MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1}
@@ -36,6 +37,7 @@ __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6}
__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__=${MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT:=4000000}
__MEMPOOL_ALLOW_UNREACHABLE__=${MEMPOOL_ALLOW_UNREACHABLE:=true}
__MEMPOOL_PRICE_UPDATES_PER_HOUR__=${MEMPOOL_PRICE_UPDATES_PER_HOUR:=1}
__MEMPOOL_MAX_TRACKED_ADDRESSES__=${MEMPOOL_MAX_TRACKED_ADDRESSES:=1}
# CORE_RPC
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
@@ -53,7 +55,7 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
# ESPLORA
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"}
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:=""}
__ESPLORA_BATCH_QUERY_BASE_SIZE__=${ESPLORA_BATCH_QUERY_BASE_SIZE:=1000}
__ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
__ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000}
@@ -169,6 +171,7 @@ sed -i "s!__MEMPOOL_INITIAL_BLOCKS_AMOUNT__!${__MEMPOOL_INITIAL_BLOCKS_AMOUNT__}
sed -i "s!__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__!${__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_INDEXING_BLOCKS_AMOUNT__!${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__!${__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__}!g" mempool-config.json
sed -i "s!__MEMPOOL_GOGGLES_INDEXING__!${__MEMPOOL_GOGGLES_INDEXING__}!g" mempool-config.json
sed -i "s!__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__!${__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__}!g" mempool-config.json
sed -i "s!__MEMPOOL_EXTERNAL_ASSETS__!${__MEMPOOL_EXTERNAL_ASSETS__}!g" mempool-config.json
sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!g" mempool-config.json
@@ -188,6 +191,7 @@ sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INT
sed -i "s!__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__!${__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_ALLOW_UNREACHABLE__!${__MEMPOOL_ALLOW_UNREACHABLE__}!g" mempool-config.json
sed -i "s!__MEMPOOL_PRICE_UPDATES_PER_HOUR__!${__MEMPOOL_PRICE_UPDATES_PER_HOUR__}!g" mempool-config.json
sed -i "s!__MEMPOOL_MAX_TRACKED_ADDRESSES__!${__MEMPOOL_MAX_TRACKED_ADDRESSES__}!g" mempool-config.json
sed -i "s!__CORE_RPC_HOST__!${__CORE_RPC_HOST__}!g" mempool-config.json
sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json

7
frontend/.gitignore vendored
View File

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

View File

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

@@ -22,6 +22,7 @@ import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
import { AppPreloadingStrategy } from './app.preloading-strategy';
import { ServicesApiServices } from './services/services-api.service';
const providers = [
ElectrsApiService,
@@ -40,6 +41,7 @@ const providers = [
FiatCurrencyPipe,
CapAddressPipe,
AppPreloadingStrategy,
ServicesApiServices,
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
];

View File

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

View File

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

View File

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

View File

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

View File

@@ -416,23 +416,19 @@
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>
</div>
<p>
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&trade;, Mempool Goggles&trade;, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
</p>
<p>
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on &lt;https://mempool.space/trademark-policy&gt;.
</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 { Subscription, catchError, of, tap } from 'rxjs';
import { StorageService } from '../../services/storage.service';
import { Transaction } from '../../interfaces/electrs.interface';
import { nextRoundNumber } from '../../shared/common.utils';
import { ServicesApiServices } from '../../services/services-api.service';
import { AudioService } from '../../services/audio.service';
export type AccelerationEstimate = {
@@ -62,7 +63,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
maxRateOptions: RateOption[] = [];
constructor(
private apiService: ApiService,
private servicesApiService: ServicesApiServices,
private storageService: StorageService,
private audioService: AudioService,
private cd: ChangeDetectorRef
@@ -83,7 +84,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
ngOnInit() {
this.user = this.storageService.getAuth()?.user ?? null;
this.estimateSubscription = this.apiService.estimate$(this.tx.txid).pipe(
this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe(
tap((response) => {
if (response.status === 204) {
this.estimate = undefined;
@@ -183,7 +184,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
if (this.accelerationSubscription) {
this.accelerationSubscription.unsubscribe();
}
this.accelerationSubscription = this.apiService.accelerate$(
this.accelerationSubscription = this.servicesApiService.accelerate$(
this.tx.txid,
this.userBid
).subscribe({
@@ -213,4 +214,4 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
onResize(): void {
this.isMobile = window.innerWidth <= 767.98;
}
}
}

View File

@@ -27,12 +27,6 @@
</form>
</div>
<div *ngIf="widget">
<div class="item">
<h5 class="card-title" i18n="acceleration.total-bid-boost">Total Bid Boost</h5>
</div>
</div>
<div [class.chart]="!widget" [class.chart-widget]="widget" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>

View File

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

View File

@@ -1,8 +1,7 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnDestroy, OnInit } from '@angular/core';
import { EChartsOption, graphic } from 'echarts';
import { Observable, Subscription, combineLatest } from 'rxjs';
import { Observable, Subscription, combineLatest, fromEvent } from 'rxjs';
import { map, max, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../../services/api.service';
import { SeoService } from '../../../services/seo.service';
import { formatNumber } from '@angular/common';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
@@ -11,6 +10,8 @@ import { StorageService } from '../../../services/storage.service';
import { MiningService } from '../../../services/mining.service';
import { ActivatedRoute } from '@angular/router';
import { Acceleration } from '../../../interfaces/node-api.interface';
import { ServicesApiServices } from '../../../services/services-api.service';
import { ApiService } from '../../../services/api.service';
@Component({
selector: 'app-acceleration-fees-graph',
@@ -28,6 +29,7 @@ import { Acceleration } from '../../../interfaces/node-api.interface';
})
export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
@Input() widget: boolean = false;
@Input() height: number | string = '200';
@Input() right: number | string = 45;
@Input() left: number | string = 75;
@Input() accelerations$: Observable<Acceleration[]>;
@@ -54,6 +56,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
@Inject(LOCALE_ID) public locale: string,
private seoService: SeoService,
private apiService: ApiService,
private servicesApiService: ServicesApiServices,
private formBuilder: UntypedFormBuilder,
private storageService: StorageService,
private miningService: MiningService,
@@ -66,15 +69,15 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
this.seoService.setTitle($localize`:@@bcf34abc2d9ed8f45a2f65dd464c46694e9a181e:Acceleration Fees`);
this.isLoading = true;
if (this.widget) {
this.miningWindowPreference = '1m';
this.timespan = this.miningWindowPreference;
this.statsObservable$ = combineLatest([
(this.accelerations$ || this.apiService.getAccelerationHistory$({ timeframe: this.miningWindowPreference })),
(this.accelerations$ || this.servicesApiService.getAccelerationHistory$({ timeframe: this.miningWindowPreference })),
this.apiService.getHistoricalBlockFees$(this.miningWindowPreference),
fromEvent(window, 'resize').pipe(startWith(null)),
]).pipe(
tap(([accelerations, blockFeesResponse]) => {
this.prepareChartOptions(accelerations, blockFeesResponse.body);
@@ -86,6 +89,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
}),
);
} else {
this.seoService.setTitle($localize`:@@bcf34abc2d9ed8f45a2f65dd464c46694e9a181e:Acceleration Fees`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('1w');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@@ -101,7 +105,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
this.isLoading = true;
this.storageService.setValue('miningWindowPreference', timespan);
this.timespan = timespan;
return this.apiService.getAccelerationHistory$({});
return this.servicesApiService.getAccelerationHistory$({});
})
),
this.radioGroupForm.get('dateSpan').valueChanges.pipe(
@@ -173,6 +177,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
],
animation: false,
grid: {
height: this.height,
right: this.right,
left: this.left,
bottom: this.widget ? 30 : 80,

View File

@@ -63,66 +63,82 @@ tr, td, th {
}
.txid {
width: 25%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 30%;
@media (max-width: 1060px) and (min-width: 768px) {
display: none;
}
@media (max-width: 500px) {
display: none;
}
}
.fee-rate {
width: 20%;
@media (max-width: 1060px) and (min-width: 768px) {
text-align: start !important;
}
@media (max-width: 500px) {
text-align: start !important;
}
@media (max-width: 840px) and (min-width: 768px) {
display: none;
}
@media (max-width: 410px) {
display: none;
.fee, .block, .status {
width: 15%;
@media (max-width: 720px) {
width: 20%;
}
}
.bid {
width: 30%;
min-width: 150px;
@media (max-width: 840px) and (min-width: 768px) {
text-align: start !important;
.widget {
.txid {
width: 30%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 30%;
@media (max-width: 1060px) and (min-width: 768px) {
display: none;
}
@media (max-width: 500px) {
display: none;
}
}
@media (max-width: 410px) {
text-align: start !important;
.fee-rate {
width: 20%;
@media (max-width: 1060px) and (min-width: 768px) {
text-align: start !important;
}
@media (max-width: 500px) {
text-align: start !important;
}
@media (max-width: 840px) and (min-width: 768px) {
display: none;
}
@media (max-width: 410px) {
display: none;
}
}
}
.time {
width: 25%;
}
.fee {
width: 35%;
@media (max-width: 1060px) and (min-width: 768px) {
text-align: start !important;
.bid {
width: 30%;
min-width: 150px;
@media (max-width: 840px) and (min-width: 768px) {
text-align: start !important;
}
@media (max-width: 410px) {
text-align: start !important;
}
}
@media (max-width: 500px) {
text-align: start !important;
.time {
width: 25%;
}
}
.block {
width: 20%;
}
.fee {
width: 30%;
@media (max-width: 1060px) and (min-width: 768px) {
text-align: start !important;
}
@media (max-width: 500px) {
text-align: start !important;
}
}
.status {
width: 20%
.block {
width: 20%;
}
.status {
width: 20%
}
}
/* Tooltip text */

View File

@@ -1,9 +1,9 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core';
import { Observable, catchError, of, switchMap, tap } from 'rxjs';
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
import { ApiService } from '../../../services/api.service';
import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service';
import { ServicesApiServices } from '../../../services/services-api.service';
@Component({
selector: 'app-accelerations-list',
@@ -26,7 +26,7 @@ export class AccelerationsListComponent implements OnInit {
skeletonLines: number[] = [];
constructor(
private apiService: ApiService,
private servicesApiService: ServicesApiServices,
private websocketService: WebsocketService,
public stateService: StateService,
private cd: ChangeDetectorRef,
@@ -41,7 +41,7 @@ export class AccelerationsListComponent implements OnInit {
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
const accelerationObservable$ = this.accelerations$ || (this.pending ? this.apiService.getAccelerations$() : this.apiService.getAccelerationHistory$({ timeframe: '1m' }));
const accelerationObservable$ = this.accelerations$ || (this.pending ? this.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistory$({ timeframe: '1m' }));
this.accelerationList$ = accelerationObservable$.pipe(
switchMap(accelerations => {
if (this.pending) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -77,6 +77,49 @@
}
}
&.any-mode {
.filter-tag {
border: solid 1px #1a9436;
&.selected {
background-color: #1a9436;
}
}
}
.btn-group {
font-size: 0.9em;
margin-right: 0.25em;
}
.mode-toggle {
padding: 0.2em 0.5em;
pointer-events: all;
line-height: 1.5;
background: #181b2daf;
&:first-child {
border-top-left-radius: 0.2rem;
border-bottom-left-radius: 0.2rem;
}
&:last-child {
border-top-right-radius: 0.2rem;
border-bottom-right-radius: 0.2rem;
}
&.blue {
border: solid 1px #105fb0;
&.active {
background: #105fb0;
}
}
&.green {
border: solid 1px #1a9436;
&.active {
background: #1a9436;
}
}
}
:host-context(.block-overview-graph:hover) &, &:hover, &:active {
.menu-toggle {
opacity: 0.5;
@@ -132,6 +175,11 @@
.filter-tag {
font-size: 0.7em;
}
.mode-toggle {
font-size: 0.7em;
margin-bottom: 5px;
margin-top: 2px;
}
}
&.tiny {

View File

@@ -1,5 +1,5 @@
import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core';
import { FilterGroups, TransactionFilters } from '../../shared/filters.utils';
import { ActiveFilter, FilterGroups, FilterMode, TransactionFilters } from '../../shared/filters.utils';
import { StateService } from '../../services/state.service';
import { Subscription } from 'rxjs';
@@ -12,7 +12,7 @@ import { Subscription } from 'rxjs';
export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
@Input() cssWidth: number = 800;
@Input() excludeFilters: string[] = [];
@Output() onFilterChanged: EventEmitter<bigint | null> = new EventEmitter();
@Output() onFilterChanged: EventEmitter<ActiveFilter | null> = new EventEmitter();
filterSubscription: Subscription;
@@ -21,6 +21,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
disabledFilters: { [key: string]: boolean } = {};
activeFilters: string[] = [];
filterFlags: { [key: string]: boolean } = {};
filterMode: FilterMode = 'and';
menuOpen: boolean = false;
constructor(
@@ -29,15 +30,16 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
) {}
ngOnInit(): void {
this.filterSubscription = this.stateService.activeGoggles$.subscribe((activeFilters: string[]) => {
this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => {
this.filterMode = active.mode;
for (const key of Object.keys(this.filterFlags)) {
this.filterFlags[key] = false;
}
for (const key of activeFilters) {
for (const key of active.filters) {
this.filterFlags[key] = !this.disabledFilters[key];
}
this.activeFilters = [...activeFilters.filter(key => !this.disabledFilters[key])];
this.onFilterChanged.emit(this.getBooleanFlags());
this.activeFilters = [...active.filters.filter(key => !this.disabledFilters[key])];
this.onFilterChanged.emit({ mode: active.mode, filters: this.activeFilters });
});
}
@@ -53,6 +55,12 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
}
}
setFilterMode(mode): void {
this.filterMode = mode;
this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters });
this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] });
}
toggleFilter(key): void {
const filter = this.filters[key];
this.filterFlags[key] = !this.filterFlags[key];
@@ -73,8 +81,8 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
this.activeFilters = this.activeFilters.filter(f => f != key);
}
const booleanFlags = this.getBooleanFlags();
this.onFilterChanged.emit(booleanFlags);
this.stateService.activeGoggles$.next([...this.activeFilters]);
this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters });
this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] });
}
getBooleanFlags(): bigint | null {
@@ -90,7 +98,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
@HostListener('document:click', ['$event'])
onClick(event): boolean {
// click away from menu
if (!event.target.closest('button')) {
if (!event.target.closest('button') && !event.target.closest('label')) {
this.menuOpen = false;
}
return true;

View File

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

View File

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

View File

@@ -9,6 +9,8 @@ import { Price } from '../../services/price.service';
import { StateService } from '../../services/state.service';
import { Subscription } from 'rxjs';
import { defaultColorFunction, setOpacity, defaultFeeColors, defaultAuditFeeColors, defaultMarginalFeeColors, defaultAuditColors } from './utils';
import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils';
import { detectWebGL } from '../../shared/graphs.utils';
const unmatchedOpacity = 0.2;
const unmatchedFeeColors = defaultFeeColors.map(c => setOpacity(c, unmatchedOpacity));
@@ -42,6 +44,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() showFilters: boolean = false;
@Input() excludeFilters: string[] = [];
@Input() filterFlags: bigint | null = null;
@Input() filterMode: FilterMode = 'and';
@Input() blockConversion: Price;
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
@@ -75,11 +78,14 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
filtersAvailable: boolean = true;
activeFilterFlags: bigint | null = null;
webGlEnabled = true;
constructor(
readonly ngZone: NgZone,
readonly elRef: ElementRef,
private stateService: StateService,
) {
this.webGlEnabled = detectWebGL();
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
this.searchSubscription = this.stateService.searchText$.subscribe((text) => {
this.searchText = text;
@@ -113,16 +119,17 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
if (changes.overrideColor && this.scene) {
this.scene.setColorFunction(this.overrideColors);
}
if ((changes.filterFlags || changes.showFilters)) {
if ((changes.filterFlags || changes.showFilters || changes.filterMode)) {
this.setFilterFlags();
}
}
setFilterFlags(flags?: bigint | null): void {
this.activeFilterFlags = this.filterFlags || flags || null;
setFilterFlags(goggle?: ActiveFilter): void {
this.filterMode = goggle?.mode || this.filterMode;
this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags;
if (this.scene) {
if (flags != null) {
this.scene.setColorFunction(this.getFilterColorFunction(flags));
if (this.activeFilterFlags != null && this.filtersAvailable) {
this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags));
} else {
this.scene.setColorFunction(this.overrideColors);
}
@@ -156,7 +163,11 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
// initialize the scene without any entry transition
setup(transactions: TransactionStripped[]): void {
this.filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
if (filtersAvailable !== this.filtersAvailable) {
this.setFilterFlags();
}
this.filtersAvailable = filtersAvailable;
if (this.scene) {
this.scene.setup(transactions);
this.readyNextFrame = true;
@@ -499,11 +510,13 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
onTxClick(cssX: number, cssY: number, keyModifier: boolean = false) {
const x = cssX * window.devicePixelRatio;
const y = cssY * window.devicePixelRatio;
const selected = this.scene.getTxAt({ x, y });
if (selected && selected.txid) {
this.txClickEvent.emit({ tx: selected, keyModifier });
if (this.scene) {
const x = cssX * window.devicePixelRatio;
const y = cssY * window.devicePixelRatio;
const selected = this.scene.getTxAt({ x, y });
if (selected && selected.txid) {
this.txClickEvent.emit({ tx: selected, keyModifier });
}
}
}
@@ -523,7 +536,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) {
return (tx: TxView) => {
if ((tx.bigintFlags & flags) === flags) {
if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) {
return defaultColorFunction(tx);
} else {
return defaultColorFunction(

View File

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

@@ -10,6 +10,7 @@ import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.in
import { ApiService } from '../../services/api.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
import { ServicesApiServices } from '../../services/services-api.service';
@Component({
selector: 'app-block-preview',
@@ -42,7 +43,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
public stateService: StateService,
private seoService: SeoService,
private openGraphService: OpenGraphService,
private apiService: ApiService
private apiService: ApiService,
private servicesApiService: ServicesApiServices,
) { }
ngOnInit() {
@@ -134,7 +136,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
return of(transactions);
})
),
this.stateService.env.ACCELERATOR === true && block.height > 819500 ? this.apiService.getAccelerationHistory$({ blockHash: block.id }) : of([])
this.stateService.env.ACCELERATOR === true && block.height > 819500 ? this.servicesApiService.getAccelerationHistory$({ blockHash: block.id }) : of([])
]);
}
),

View File

@@ -59,7 +59,7 @@
<td [innerHTML]="'&lrm;' + (block.weight | wuBytes: 2)"></td>
</tr>
<tr *ngIf="auditAvailable">
<td><ng-container i18n="latest-blocks.health">Health</ng-container>&nbsp;<a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="what-is-block-health"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></td>
<td><ng-container i18n="latest-blocks.health">Health</ng-container><a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="what-is-block-health"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></td>
<td>
<span
class="health-badge badge"
@@ -242,7 +242,7 @@
</ng-container>
</div>
<div class="col-sm" *ngIf="!isMobile">
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container><a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
<div class="block-graph-wrapper">
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="86"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" [auditHighlighting]="showAudit"

View File

@@ -16,6 +16,7 @@ import { detectWebGL } from '../../shared/graphs.utils';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { PriceService, Price } from '../../services/price.service';
import { CacheService } from '../../services/cache.service';
import { ServicesApiServices } from '../../services/services-api.service';
@Component({
selector: 'app-block',
@@ -103,6 +104,7 @@ export class BlockComponent implements OnInit, OnDestroy {
private apiService: ApiService,
private priceService: PriceService,
private cacheService: CacheService,
private servicesApiService: ServicesApiServices,
) {
this.webGlEnabled = detectWebGL();
}
@@ -329,7 +331,7 @@ export class BlockComponent implements OnInit, OnDestroy {
return of(null);
})
),
this.stateService.env.ACCELERATOR === true && block.height > 819500 ? this.apiService.getAccelerationHistory$({ blockHash: block.id }) : of([])
this.stateService.env.ACCELERATOR === true && block.height > 819500 ? this.servicesApiService.getAccelerationHistory$({ blockHash: block.id }) : of([])
]);
})
)

View File

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

View File

@@ -92,21 +92,18 @@
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td *ngIf="isMempoolModule" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
<span class="skeleton-loader" style="max-width: 125px"></span>
<span class="skeleton-loader" style="max-width: 150px"></span>
</td>
<td class="timestamp" *ngIf="!widget" [class]="isMempoolModule ? '' : 'legacy'">
<span class="skeleton-loader" style="max-width: 150px"></span>
</td>
<td class="mined" *ngIf="!widget" [class]="isMempoolModule ? '' : 'legacy'">
<span class="skeleton-loader" style="max-width: 125px"></span>
</td>
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td *ngIf="isMempoolModule" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td *ngIf="isMempoolModule && !widget" class="fees text-right" [class]="isMempoolModule ? '' : 'legacy'">
<td *ngIf="isMempoolModule && !auditAvailable || isMempoolModule && !widget" class="fees text-right" [class]="isMempoolModule ? '' : 'legacy'">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td *ngIf="auditAvailable && !widget" class="fee-delta" [class]="isMempoolModule ? '' : 'legacy'">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -78,7 +78,7 @@
<li class="nav-item" routerLinkActive="active" id="btn-assets">
<a class="nav-link" [routerLink]="['/assets' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'database']" [fixedWidth]="true" i18n-title="master-page.assets" title="Assets"></fa-icon></a>
</li>
<li [hidden]="isMobile" class="nav-item mr-2" routerLinkActive="active" id="btn-docs">
<li class="nav-item mr-2" routerLinkActive="active" id="btn-docs">
<a class="nav-link" [routerLink]="['/docs' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'book']" [fixedWidth]="true" i18n-title="master-page.docs" title="Docs"></fa-icon></a>
</li>
<li class="nav-item" routerLinkActive="active" id="btn-about">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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