Compare commits

...

290 Commits

Author SHA1 Message Date
softsimon
cd5a51098b Merge pull request #4419 from mempool/nymkappa/fix-about-page-ctas
[about page] fix wrapping perk
2023-11-26 18:24:58 +09:00
softsimon
d3f8876818 Merge pull request #4424 from mempool/nymkappa/channel-map-larger
[lightning] enlarge channel map
2023-11-26 18:09:31 +09:00
softsimon
be2b9a9c2e Merge pull request #4436 from mempool/simon/pull-from-transifex-11-25
Pull from transifex 11/25
2023-11-26 17:01:10 +09:00
softsimon
63ba273dbe Pull from transifex 11/25 2023-11-26 17:00:46 +09:00
softsimon
8ae4c75c1a Fixing one last i18n string 2023-11-26 16:59:45 +09:00
softsimon
1cece1037a Disclaimer i18n fix 2023-11-26 16:29:20 +09:00
softsimon
478873302c Merging another duplicate i18n string 2023-11-26 16:20:57 +09:00
softsimon
3cff01b21e Merging another duplicate i18n string 2023-11-26 16:20:46 +09:00
softsimon
f67848b043 Merging another duplicate i18n string 2023-11-26 16:10:18 +09:00
softsimon
f0c88ff6cc Fix duplicate recent block i18n 2023-11-26 16:03:53 +09:00
softsimon
da5adc3e4e Merge pull request #4435 from mempool/simon/more-i18n-fixes3
More i18n fixes (3)
2023-11-26 15:49:02 +09:00
softsimon
bebd2ea028 More i18n fixes (3) 2023-11-26 15:48:44 +09:00
softsimon
e7e25e1632 Merge pull request #4432 from mempool/simon/more-i18n-fixes-etc
RBF i18n fixes
2023-11-25 20:46:19 +09:00
softsimon
c4130fd5b9 RBF i18n fixes 2023-11-25 20:45:59 +09:00
softsimon
dcad18b297 Merge pull request #4431 from mempool/simon/seo-description-fixes
Fixing titles and merging more i18n duplicates
2023-11-25 20:31:56 +09:00
softsimon
d380aad98c Fixing titles and merging more i18n duplicates 2023-11-25 20:31:29 +09:00
softsimon
107e0be59f Merge pull request #4428 from mempool/simon/i18n-fixes-231125
Updating and correcting i18n strings
2023-11-25 18:12:17 +09:00
softsimon
e654170d0b Updating and correcting i18n strings 2023-11-25 18:05:08 +09:00
wiz
d3055dab54 Merge pull request #4157 from mempool/hunicus/bimi-svg
Add bimi svg
2023-11-25 16:29:29 +09:00
nymkappa
e95d5a7982 [lightning] enlarge channel map 2023-11-24 18:14:16 +09:00
wiz
5d5e9e8219 Legal: update Privacy Policy 20231123 2023-11-23 22:23:06 +09:00
wiz
99fd4500e4 Fix broken sponsor links on About page 2023-11-23 21:50:27 +09:00
nymkappa
69081ed647 [about page] fix wrapping perk 2023-11-23 21:07:21 +09:00
softsimon
c8bcd4f04f Merge pull request #4415 from mempool/nymkappa/fix-css-testnet
[ui] only load empty sidebar in foss mainnet
2023-11-23 18:56:43 +09:00
nymkappa
7f4fd83ad2 [ui] fix testnets padding 2023-11-23 18:40:25 +09:00
softsimon
70722dfc9c Merge pull request #4414 from mempool/nymkappa/fix-waitlist-message-preview
[accelerator] fix preview UX on mobile when there is an error/warning
2023-11-23 17:42:19 +09:00
softsimon
407b4c53a6 Merge pull request #4403 from mempool/mononaut/empty-mempool-block-health
Fix health score when mempool was empty
2023-11-23 17:25:06 +09:00
nymkappa
c11551de7b [accelerator] fix preview UX on mobile when there is an error/warning 2023-11-23 17:00:57 +09:00
softsimon
2d5964b81e Merge pull request #4413 from mempool/nymkappa/polish-accel-preview
[accelerator] small align tweak
2023-11-23 15:51:07 +09:00
nymkappa
98e3c7b9cf [accelerator] small align tweak 2023-11-23 15:47:28 +09:00
softsimon
2de57a8074 Merge pull request #4412 from mempool/nymkappa/polish-accel-preview
[accelerator] fix preview text wrap not working on safari/firefox + polish accelerator preview
2023-11-23 15:39:24 +09:00
nymkappa
8badacf123 [accelerator] fix preview text wrap not working on safari/firefox
[accelerator] polish accelerator preview
2023-11-23 14:54:50 +09:00
softsimon
40b387a1e0 Merge pull request #4411 from mempool/nymkappa/fix-menu-css
[menu] force background color when menu is open
2023-11-23 13:31:11 +09:00
nymkappa
e5d2788736 [menu] force background color when menu is open 2023-11-23 10:45:16 +09:00
Mononaut
75cc844676 actually fix empty mempool health score 2023-11-22 19:53:22 +09:00
wiz
5d05dd7089 Merge pull request #4410 from mempool/simon/accelerate-button-height
Accelerate button height align fix
2023-11-22 19:51:49 +09:00
softsimon
35e108aa1c Accelerate button height fix 2023-11-22 19:37:44 +09:00
softsimon
3f06b38767 Merge pull request #4408 from mempool/mononaut/restore-calculator
Restore the calculator
2023-11-22 18:19:39 +09:00
Mononaut
9844c3d275 Restore the calculator 2023-11-22 18:13:41 +09:00
wiz
04eeb19bb9 Fix string in community sponsor CTA button box 2023-11-22 17:56:56 +09:00
wiz
671540af78 Merge pull request #4407 from mempool/mononaut/8chain
Standalone multi-block view page
2023-11-22 17:54:38 +09:00
Mononaut
1a732f18fc Remove font, update 8 block defaults, fix deprecated css 2023-11-22 17:47:51 +09:00
Mononaut
729fb3bb9d Move standalone blocks page to /view/blocks 2023-11-22 17:47:51 +09:00
Mononaut
fc56f273d4 Simplify 8chain block info 2023-11-22 17:47:51 +09:00
Mononaut
642be969a3 Add more block info, reduce font size 2023-11-22 17:47:50 +09:00
Mononaut
d69cdacd5e Add numBlocks param 2023-11-22 17:47:50 +09:00
Mononaut
ec8fc53dcb eight blocks 2023-11-22 17:47:49 +09:00
wiz
108d1762d6 Merge pull request #4404 from mempool/simon/truncate-text-link-fix
Fixes truncated links
2023-11-22 17:17:51 +09:00
wiz
879039ca8c Merge pull request #4406 from mempool/wiz/update-privacy-policy
Update our Privacy Policy for new backend
2023-11-22 17:16:38 +09:00
wiz
047d1463e7 Add missing <br> tag in privacy poilicy 2023-11-22 17:15:39 +09:00
wiz
8fc60fa086 Merge pull request #4405 from mempool/mononaut/simplify-acceleration-preview
simplify acceleration preview summary
2023-11-22 17:12:26 +09:00
wiz
bce68ee37f Merge branch 'master' into wiz/update-privacy-policy 2023-11-22 17:09:14 +09:00
wiz
2f25c128c1 Merge pull request #4395 from mempool/hunicus/about-sponsor-link
Change sponsor cta on about page
2023-11-22 17:06:55 +09:00
wiz
a76a600d3f Tweak community sponsor word 2023-11-22 17:06:02 +09:00
wiz
afdb419beb Update our Privacy Policy for new backend 2023-11-22 16:55:40 +09:00
Mononaut
8bd2aa3dd3 simplify acceleration preview summary 2023-11-22 16:38:58 +09:00
hunicus
a3f2c42b8e Replace vip events with early accelerator 2023-11-22 16:31:24 +09:00
softsimon
dbcd900056 Fixes truncated links 2023-11-22 16:22:41 +09:00
softsimon
5a6d6fae41 Merge pull request #4401 from mempool/mononaut/menu-footer-align-bug
Fix truncated hidden text layout flow bug
2023-11-22 15:35:26 +09:00
softsimon
6bb666ba5e Merge pull request #4402 from mempool/dependabot/npm_and_yarn/frontend/cypress-13.6.0
Bump cypress from 13.5.0 to 13.6.0 in /frontend
2023-11-22 14:47:49 +09:00
Mononaut
8dc80eadf3 Fix health score when mempool was empty 2023-11-22 13:10:53 +09:00
softsimon
505532f812 Merge pull request #4396 from mempool/mononaut/graph-tooltip-fixes
Fix mempool graph tooltip width & vb precision
2023-11-22 12:21:03 +09:00
dependabot[bot]
89eb02dad0 Bump cypress from 13.5.0 to 13.6.0 in /frontend
Bumps [cypress](https://github.com/cypress-io/cypress) from 13.5.0 to 13.6.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v13.5.0...v13.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-22 02:21:44 +00:00
Mononaut
94a7b710c5 Fix truncated hidden text layout flow bug 2023-11-22 11:15:00 +09:00
softsimon
1e01f88c15 Merge pull request #4398 from mempool/mononaut/fix-left-margin
Fix negative left margin on master page
2023-11-21 20:41:38 +09:00
Mononaut
d471616a24 Fix negative left margin on master page 2023-11-21 20:29:41 +09:00
hunicus
9b8f70a0ae Change alignment and copy 2023-11-21 18:00:22 +09:00
Mononaut
ab079e9372 Fix mempool graph tooltip width & vb precision 2023-11-21 16:36:46 +09:00
hunicus
b77a16233b Change sponsor cta on about page 2023-11-21 14:56:53 +09:00
wiz
894075493b ops: Fix mempool backend config for liquid socket paths 2023-11-20 23:28:37 +09:00
wiz
954512cd8e Merge pull request #4303 from mempool/nymkappa/mega-branch
[Accelerator] Mega branch
2023-11-20 22:41:23 +09:00
nymkappa
deaf6ad6a5 [enterprise] fix api endpoints urls 2023-11-20 19:12:56 +09:00
nymkappa
98443b48ba Merge remote-tracking branch 'origin/nymkappa/mega-branch' into nymkappa/mega-branch 2023-11-20 17:16:44 +09:00
nymkappa
a7dc8793c2 [about page] un-hide sponsors buttons 2023-11-20 17:16:27 +09:00
wiz
84b8e5d472 Merge branch 'master' into nymkappa/mega-branch 2023-11-20 17:15:01 +09:00
softsimon
e768072799 Merge pull request #4388 from mempool/mononaut/rbf-cache-load-logs
More verbose RBF cache check logs
2023-11-19 17:34:00 +09:00
nymkappa
206706180f [accelerator] fix confirmation message when accelerating 2023-11-19 17:23:46 +09:00
nymkappa
0e3b3a0e00 [accelerator] fix message when trying to accelerate 2023-11-19 17:16:05 +09:00
wiz
28733c1e97 Merge branch 'master' into nymkappa/mega-branch 2023-11-19 16:07:03 +09:00
softsimon
7e74a26de2 Merge pull request #4257 from mempool/nymkappa/sticky-header-non-bitcoin
Sticky header non bitcoin
2023-11-19 15:27:52 +09:00
nymkappa
6d920e0ed3 [accelerator] hide accelerate banner 2023-11-18 18:36:17 +09:00
nymkappa
88e5f8a6af remove upgrade link when feature not available 2023-11-18 17:15:04 +09:00
nymkappa
ae82ac8368 Merge remote-tracking branch 'origin/nymkappa/mega-branch' into nymkappa/mega-branch 2023-11-18 15:21:01 +09:00
nymkappa
15cba58144 /image -> /images 2023-11-18 15:20:53 +09:00
wiz
2c820f1cc0 ops: Rewrite nginx config for new services API endpoints 2023-11-18 15:09:02 +09:00
wiz
4070492584 ops: Set nginx to ignore Pragma header for no-cache endpoints 2023-11-18 13:22:17 +09:00
wiz
82a43e25e0 ops: Set nginx no-cache headers for /api/v1/services/auth 2023-11-18 12:40:39 +09:00
wiz
5e45d8f3bc ops: Set nginx no-cache headers for /api/v1/services/account 2023-11-18 12:20:18 +09:00
wiz
e5709235f3 Merge branch 'master' into nymkappa/mega-branch 2023-11-18 11:30:35 +09:00
softsimon
59c513f2a5 Merge pull request #4389 from ncois/fix-p2pk-balance
Fix P2PK balance display in address page (duplicate)
2023-11-17 20:49:02 +09:00
ncois
76a07315f3 Fix P2PK balance display in address page 2023-11-17 09:52:48 +01:00
softsimon
5723f167df Merge pull request #4386 from mempool/mononaut/fix-electrum-p2pk
Fix electrum p2pk/scripthash format
2023-11-17 17:31:22 +09:00
Mononaut
6ac328c979 More verbose RBF cache check logs 2023-11-17 07:06:44 +00:00
softsimon
85e52d24c3 Merge pull request #4382 from shubhamkmr04/shubham/Change-ZEUS-logo
Change ZEUS Logo
2023-11-17 15:29:13 +09:00
Mononaut
7717c15666 Fix electrum p2pk/scripthash format 2023-11-17 03:49:31 +00:00
softsimon
61d7fd490a Merge pull request #4385 from mempool/mononaut/memory-usage-precision
Round memory usage to 3 significant figures
2023-11-17 11:48:44 +09:00
Mononaut
ccf952983b Round memory usage to 3sf 2023-11-17 02:22:57 +00:00
Shubham Kumar
602d7ce207 Merge branch 'master' into shubham/Change-ZEUS-logo 2023-11-16 12:42:19 +05:30
nymkappa
64a51803c4 Merge branch 'master' into nymkappa/mega-branch 2023-11-16 16:10:55 +09:00
shubham
a63e68e9e3 Change ZEUS Logo 2023-11-16 12:38:54 +05:30
softsimon
d4d17fa167 Merge pull request #4371 from mempool/nymkappa/outliers-toggle
[graph] add toggle to show/hide outliers in transaction vBytes per second graph
2023-11-16 14:59:50 +09:00
nymkappa
4cd8d70de5 cleanup if/else 2023-11-15 18:47:56 +09:00
nymkappa
dc26c6f105 [graph] don't change yaxis scale if no outliers - save state in localstorage 2023-11-15 18:46:33 +09:00
nymkappa
365954f5b4 Merge pull request #4304 from mempool/nymkappa/new-enterprise-launch
Prepare enterprise only launch
2023-11-15 18:24:48 +09:00
nymkappa
ff38073280 Merge branch 'nymkappa/mega-branch' into nymkappa/new-enterprise-launch 2023-11-15 18:22:37 +09:00
nymkappa
eeebfde33c Merge branch 'master' into nymkappa/mega-branch 2023-11-15 18:22:27 +09:00
softsimon
fa12233667 Merge pull request #4381 from mempool/mononaut/acc-preview-min-height
Minimum acceleration preview bar height
2023-11-15 18:22:05 +09:00
nymkappa
19477c4ee3 Merge branch 'master' into mononaut/acc-preview-min-height 2023-11-15 18:18:42 +09:00
nymkappa
e4b56bac88 Merge branch 'nymkappa/mega-branch' into nymkappa/new-enterprise-launch 2023-11-15 18:18:01 +09:00
nymkappa
d390fa8671 Merge branch 'master' into nymkappa/mega-branch 2023-11-15 18:17:48 +09:00
softsimon
f9f9c62608 Merge pull request #4374 from mempool/nymkappa/fix-hybrid-build-routing
[frontend] export MasterPageComponent for re-use in hybrid build
2023-11-15 18:02:25 +09:00
Mononaut
d9966143c1 Minimum acceleration preview bar height 2023-11-15 08:49:22 +00:00
nymkappa
57ab82ae7a Merge branch 'nymkappa/mega-branch' into nymkappa/new-enterprise-launch 2023-11-15 17:23:42 +09:00
nymkappa
756f6d8abe [frontend] export MasterPageComponent for re-use in hybrid build 2023-11-15 17:09:54 +09:00
softsimon
f0840a51d9 Merge pull request #4379 from mempool/mononaut/reduce-batch-sizes
Configurable mempool/electrs batch sizes
2023-11-15 16:56:35 +09:00
softsimon
eac8f8c2c6 Merge pull request #4368 from mempool/simon/truncate-selection
Make search and select work for truncated text
2023-11-15 16:25:17 +09:00
Mononaut
20f61fc6a0 Round batch sizes up 2023-11-15 06:58:00 +00:00
Mononaut
6454892d48 Use mempool/txs max_txs parameter 2023-11-15 06:57:31 +00:00
Mononaut
35d7c55c1d Configurable esplora batch sizes 2023-11-15 06:12:15 +00:00
softsimon
86fe6a802b Fixing mobile overflow 2023-11-15 15:07:14 +09:00
softsimon
d4568b631d Make search and select work for truncated text
fixes #4367
2023-11-15 14:39:23 +09:00
nymkappa
2d30c0b588 [graph] use echart echart yaxis max property instead of modifying the data itself 2023-11-15 14:08:44 +09:00
wiz
1aea3fcac5 Merge pull request #4378 from mempool/mononaut/tomahawk-logs-fix
Set fallback server out-of-sync when unreachable
2023-11-14 19:27:13 +09:00
Mononaut
5cfd599018 Set fallback server out-of-symc when unreachable 2023-11-14 10:17:02 +00:00
wiz
d8f2462ff0 Merge pull request #4377 from mempool/mononaut/tomahawk-logs
🥩🙂🪵📝
2023-11-14 19:10:41 +09:00
Mononaut
85091e1f3a 🥩🙂🪵📝 2023-11-14 09:55:02 +00:00
softsimon
3a8d19062f Merge pull request #4271 from mempool/mononaut/refactor-task-scheduler
Refactor indexer scheduling to avoid accumulating identical tasks
2023-11-14 18:13:48 +09:00
wiz
6913946079 ops: Upgrade mariadb to 10.11.x 2023-11-14 17:46:52 +09:00
wiz
fdd18317f9 ops: Kill node before sh in stop script 2023-11-14 17:46:42 +09:00
softsimon
a7c64c0df3 Merge branch 'master' into mononaut/refactor-task-scheduler 2023-11-14 17:17:55 +09:00
softsimon
f2fb2f98f1 Merge pull request #4375 from mempool/mononaut/db-error-log-levels
Support different log levels for database query error messages
2023-11-14 17:17:31 +09:00
Mononaut
7aad664112 Support different log levels for database query error messages 2023-11-14 07:59:24 +00:00
nymkappa
fa040ca19f [frontend] export MasterPageComponent for re-use in hybrid build 2023-11-14 15:53:37 +09:00
Mononaut
00887bc24b Refactor indexer scheduling to avoid accumulating identical tasks 2023-11-14 06:30:22 +00:00
softsimon
ab8b557e73 Merge pull request #4373 from mempool/mononaut/fix-silent-db-exceptions
Fix silently unhandled database exceptions
2023-11-14 15:27:52 +09:00
Mononaut
5c0a59d2f6 handle unknown query types in db error log 2023-11-14 06:13:06 +00:00
Mononaut
29cbdf6cd5 handle null query in error log 2023-11-14 05:52:27 +00:00
Mononaut
08b68ef8ba handle exception in db transaction rollback 2023-11-14 05:33:48 +00:00
Mononaut
1ae34e069c Fix silently unhandled database exceptions 2023-11-14 05:11:51 +00:00
softsimon
5bad829afc Merge pull request #4316 from starius/taproot-channels
[lightning] add taproot-channels to node features
2023-11-14 13:54:31 +09:00
softsimon
562cd5683a Merge branch 'master' into taproot-channels 2023-11-14 13:41:44 +09:00
nymkappa
cbf2395009 Merge branch 'master' into nymkappa/mega-branch 2023-11-14 11:12:11 +09:00
nymkappa
c393483590 [graph] add toggle to show/hide outliers in transaction vBytes per second graph 2023-11-14 10:51:51 +09:00
wiz
cbe1ec4e72 Merge pull request #4356 from mempool/mononaut/fix-pid-lock
Recover from stale PID file
2023-11-13 22:19:43 +09:00
Mononaut
c6a92083a8 Fix pid parsing on release lock 2023-11-13 07:54:11 +00:00
Mononaut
8c4b488251 handle SIGHUP exit code 2023-11-13 07:54:11 +00:00
Mononaut
3639dcc92a Recover from stale PID file 2023-11-13 07:54:11 +00:00
wiz
28113d1141 Merge pull request #4163 from mempool/mononaut/fast-forensics
Fast lightning forensics
2023-11-13 14:54:20 +09:00
softsimon
12b49abfdd Merge pull request #4258 from mempool/mononaut/esplora-sigops
Sigops everywhere
2023-11-13 14:32:23 +09:00
wiz
68e0dfe736 Merge branch 'master' into mononaut/fast-forensics 2023-11-13 14:24:08 +09:00
Mononaut
29299e622e Calculate sigops in /api/v1/tx/:txid for mined txs 2023-11-13 03:44:35 +00:00
Mononaut
4ac0a6dad2 Display sigops on all transactions 2023-11-13 03:44:35 +00:00
Mononaut
a510b4992c Fix condition to add mempool data to loaded cache txs 2023-11-13 03:44:35 +00:00
Mononaut
72e19f674a Clear Liquid sigops 2023-11-13 03:44:34 +00:00
Mononaut
ea2a7e7505 Use sigops from mempool/electrs, fix the nodejs sigop calculation 2023-11-13 03:44:34 +00:00
wiz
74a2cedc7a Merge pull request #4101 from mempool/mononaut/faster-rbf-load
use bulk /txs endpoint to check cached rbf tx status
2023-11-12 19:10:53 +09:00
wiz
c0a481acbe Reduce /internal/txs RBF cache query chunk size to 250 2023-11-12 19:09:36 +09:00
Mononaut
5998b54fec more logs, reduce request chunk sizes 2023-11-12 09:23:37 +00:00
wiz
a2f6ea7b3a Merge branch 'master' into mononaut/faster-rbf-load 2023-11-12 17:41:30 +09:00
wiz
6a877c2a2e Merge pull request #4158 from mempool/junderw/fix-paths
Nginx: Ignore all internal-api paths
2023-11-12 17:40:34 +09:00
wiz
ee90a96d22 Merge branch 'master' into junderw/fix-paths 2023-11-12 17:22:39 +09:00
softsimon
4ea7989aec Merge pull request #4364 from mempool/mononaut/fix-incoming-tx-chart
restore incoming tx clearance line, smooth moving avg
2023-11-12 17:13:53 +09:00
Mononaut
fc52514c31 restore incoming tx clearance line, smooth moving avg 2023-11-12 07:59:06 +00:00
Mononaut
b9217da453 Change internal API prefix to "internal" 2023-11-12 07:08:21 +00:00
Mononaut
70badaf461 Speed up $scanForClosedChannels, use internal outspends apis 2023-11-12 07:08:20 +00:00
Mononaut
995acb238d Refactor forensics batching, speed up opened channel forensics 2023-11-12 07:08:20 +00:00
Mononaut
5bee54a2bf Use new bulk endpoints to speed up forensics 2023-11-12 07:08:19 +00:00
Mononaut
7ec7ae7b95 Remove old batched outspends API 2023-11-12 07:03:28 +00:00
Mononaut
09f208484a Add electrsApiService cachedRequest function, switch outspends 2023-11-12 07:03:28 +00:00
Mononaut
8aa51c4e80 Batch esplora outspends requests 2023-11-12 07:03:28 +00:00
Mononaut
823f06451c Send correct tx conf status in websocket msgs 2023-11-12 07:03:27 +00:00
Mononaut
9d60c39aeb Resolve rbf cache merge conflicts 2023-11-12 06:19:46 +00:00
Mononaut
031e14f302 Update internal getRawTransactions to use new prefix 2023-11-12 05:37:39 +00:00
Mononaut
156b5d0b3c Update bulk /txs to use new failover router, internal-api path 2023-11-12 05:37:39 +00:00
Mononaut
38909cfc42 use bulk /txs endpoint to check cached rbf tx status 2023-11-12 05:37:37 +00:00
softsimon
3de779ea13 Merge pull request #4255 from mempool/mononaut/confirmed-tx-status
Send correct tx conf status in websocket msgs
2023-11-12 14:34:12 +09:00
Mononaut
2339a0771e Use internal /block/:hash/txs endpoint 2023-11-12 05:04:41 +00:00
junderw
4972f00a96 Add internal endpoint blocking to all Nginx configs 2023-11-12 05:04:41 +00:00
Jonathan Underwood
502a1c021e Add suggestions from wiz
Co-authored-by: wiz <j@wiz.biz>
2023-11-12 05:04:41 +00:00
Mononaut
d16773bfa0 Switch backend to use internal-api paths 2023-11-12 05:04:41 +00:00
junderw
511b827bf5 Nginx: Ignore all internal-api paths 2023-11-12 05:04:40 +00:00
softsimon
90634c4343 Merge branch 'master' into mononaut/confirmed-tx-status 2023-11-12 13:48:35 +09:00
softsimon
1e69f3b331 Merge pull request #4246 from mempool/dependabot/npm_and_yarn/frontend/mock-socket-9.3.1
Bump mock-socket from 9.2.1 to 9.3.1 in /frontend
2023-11-11 18:42:53 +09:00
dependabot[bot]
4273276422 Bump mock-socket from 9.2.1 to 9.3.1 in /frontend
Bumps [mock-socket](https://github.com/thoov/mock-socket) from 9.2.1 to 9.3.1.
- [Release notes](https://github.com/thoov/mock-socket/releases)
- [Changelog](https://github.com/thoov/mock-socket/blob/master/CHANGELOG.md)
- [Commits](https://github.com/thoov/mock-socket/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-11 09:37:11 +00:00
softsimon
fd25713746 Merge pull request #4318 from mempool/mononaut/more-dp-fees
Improve precision of fee distribution chart labels
2023-11-11 18:36:26 +09:00
Mononaut
37605d5732 Improve fee distribution legibility on small screens 2023-11-11 08:57:55 +00:00
softsimon
df2967d576 Merge pull request #4353 from mempool/dependabot/npm_and_yarn/frontend/frontend-angular-dependencies-f2c2dc5ad6
Bump the frontend-angular-dependencies group in /frontend with 1 update
2023-11-11 17:57:32 +09:00
dependabot[bot]
9ab85ab799 Bump the frontend-angular-dependencies group in /frontend with 1 update
Bumps the frontend-angular-dependencies group in /frontend with 1 update: [ngx-echarts](https://github.com/xieziyu/ngx-echarts).

- [Release notes](https://github.com/xieziyu/ngx-echarts/releases)
- [Commits](https://github.com/xieziyu/ngx-echarts/commits)

---
updated-dependencies:
- dependency-name: ngx-echarts
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-11 08:39:56 +00:00
softsimon
1b6f039341 Merge pull request #4289 from mempool/dependabot/npm_and_yarn/frontend/get-func-name-2.0.2
Bump get-func-name from 2.0.0 to 2.0.2 in /frontend
2023-11-11 17:38:47 +09:00
dependabot[bot]
0abf4f415f Bump get-func-name from 2.0.0 to 2.0.2 in /frontend
Bumps [get-func-name](https://github.com/chaijs/get-func-name) from 2.0.0 to 2.0.2.
- [Release notes](https://github.com/chaijs/get-func-name/releases)
- [Commits](https://github.com/chaijs/get-func-name/commits/v2.0.2)

---
updated-dependencies:
- dependency-name: get-func-name
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-11 08:29:45 +00:00
Mononaut
b5a5f0f608 Add small margin above fee distribution graph 2023-11-11 08:24:58 +00:00
Mononaut
ffd2685922 Display fee distribution labels to 3 sig figs 2023-11-11 08:24:57 +00:00
softsimon
9845567c2f Merge pull request #4361 from mempool/mononaut/fix-double-outspend-request
ApiService caching layer
2023-11-11 17:07:31 +09:00
Mononaut
6740ab61f3 Reduce outspend API cache expiry to 250ms 2023-11-11 07:46:25 +00:00
Mononaut
3f0c3c1952 Add apiService cachedRequest function, apply to outspends requests 2023-11-11 07:16:59 +00:00
softsimon
7142d69dda Merge pull request #4319 from mempool/mononaut/fix-stale-rbf-cache
Fix rbf tree leak, clean up stale trees in Redis
2023-11-11 15:17:13 +09:00
Mononaut
e3e248d601 Convert RBF disk cache data to match new format 2023-11-11 05:52:37 +00:00
softsimon
a0a4ae611c Merge pull request #2846 from antonilol/cookie
Add Bitcoin Core RPC cookie authentication option
2023-11-11 13:16:35 +09:00
Mononaut
0e420d8196 Fix rbf tree leak, clean up stale trees in Redis 2023-11-11 04:05:56 +00:00
softsimon
06d1bbdf03 Merge branch 'master' into cookie 2023-11-11 12:57:49 +09:00
softsimon
87b034fa8c Merge pull request #4292 from mempool/mononaut/hide-arrow-on-replace
Hide the blockchain arrow when transaction is replaced
2023-11-11 12:17:54 +09:00
softsimon
874d2b371d Merge branch 'master' into mononaut/hide-arrow-on-replace 2023-11-11 11:57:03 +09:00
softsimon
6134376964 Merge pull request #4347 from mempool/dependabot/npm_and_yarn/frontend/cypress-13.5.0
Bump cypress from 13.3.0 to 13.5.0 in /frontend
2023-11-11 11:56:46 +09:00
softsimon
eba5560caa Merge pull request #4348 from mempool/dependabot/npm_and_yarn/backend/axios-1.6.1
Bump axios from 1.5.0 to 1.6.1 in /backend
2023-11-11 11:56:34 +09:00
dependabot[bot]
e08665dee5 Bump cypress from 13.3.0 to 13.5.0 in /frontend
Bumps [cypress](https://github.com/cypress-io/cypress) from 13.3.0 to 13.5.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v13.3.0...v13.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-11 02:47:53 +00:00
softsimon
b8b341431a Merge pull request #4354 from mempool/mononaut/rerebundle
Better code-splitting
2023-11-10 16:40:36 +09:00
softsimon
9e6072c371 Merge branch 'master' into mononaut/confirmed-tx-status 2023-11-10 14:49:24 +09:00
Mononaut
af7b9c0dc8 Adjust webpack chunk preloading strategy 2023-11-10 05:36:19 +00:00
softsimon
1e3babe714 Merge pull request #4254 from mempool/mononaut/failover-timeout-config
Add REQUEST_TIMEOUT and FALLBACK_TIMEOUT esplora config options
2023-11-10 14:27:25 +09:00
softsimon
a663cca3d0 Merge branch 'master' into mononaut/failover-timeout-config 2023-11-10 14:09:43 +09:00
softsimon
3580d060ce Merge pull request #4274 from mempool/mononaut/fix-faq-blockchain
Fix faq blockchain positioning
2023-11-10 13:53:15 +09:00
softsimon
31ead371dc Merge branch 'master' into mononaut/fix-faq-blockchain 2023-11-10 13:49:58 +09:00
softsimon
897cd7edeb Merge pull request #3571 from mempool/nymkappa/tx-overflow
Fix transaction amount overflow
2023-11-10 12:56:40 +09:00
softsimon
683b72387b Merge branch 'master' into nymkappa/tx-overflow 2023-11-10 12:47:52 +09:00
Mononaut
db8ed5b705 Split liquid, lightning & mainnet graphs 2023-11-10 03:21:25 +00:00
Mononaut
80bf2f9ebd Split master page components & deduplicate main routes 2023-11-10 03:20:59 +00:00
Mononaut
854a9dd057 Split legal page components into separate modules 2023-11-10 03:20:59 +00:00
Mononaut
ae02ff5b0d Split About component into separate module 2023-11-10 03:20:59 +00:00
Mononaut
0cf898a19f Split transaction components into separate module 2023-11-10 03:20:57 +00:00
Mononaut
b5a8687b6a Split Block component into separate module 2023-11-10 03:20:32 +00:00
wiz
27077dcd6c Merge pull request #4280 from mempool/mononaut/standalone-visualizations
Standalone block visualizations
2023-11-09 20:06:28 +09:00
wiz
80ecf77270 Merge branch 'master' into mononaut/standalone-visualizations 2023-11-09 19:52:32 +09:00
wiz
8d6225d6c5 Merge pull request #4309 from mempool/knorrium/unfurler_config
Configurable unfurler config
2023-11-09 19:50:58 +09:00
wiz
8dcd372b99 Merge pull request #4312 from mempool/mononaut/fix-group-channel-count
Fix node group map channel count
2023-11-09 19:26:08 +09:00
wiz
c4d37392dc Merge branch 'master' into mononaut/fix-group-channel-count 2023-11-09 18:46:41 +09:00
wiz
4ed3b65d12 Merge pull request #4323 from mempool/mononaut/hide-mempool-count-line
Hide mempool graph count line by default
2023-11-09 18:46:33 +09:00
wiz
86458927d6 Merge branch 'master' into mononaut/hide-mempool-count-line 2023-11-09 18:38:11 +09:00
wiz
49f189f257 Merge pull request #4343 from erikarvstedt/add-distro-nix-bitcoin
README: add `nix-bitcoin` to node distros
2023-11-09 18:37:20 +09:00
softsimon
9cc0aaeac2 Merge pull request #4345 from mempool/mononaut/rebundle
Shake the echarts tree
2023-11-09 18:25:26 +09:00
wiz
2df99ef7ed Merge branch 'master' into mononaut/rebundle 2023-11-09 18:06:19 +09:00
softsimon
450bda7d04 Merge pull request #4328 from mempool/mononaut/purging-min-fee
Enforce purging rate minimum on recommended fees
2023-11-09 14:58:48 +09:00
dependabot[bot]
d18b8881d0 Bump axios from 1.5.0 to 1.6.1 in /backend
Bumps [axios](https://github.com/axios/axios) from 1.5.0 to 1.6.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.5.0...v1.6.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-09 02:16:55 +00:00
wiz
1b3ceed707 Merge pull request #4325 from fanquake/bitcoin_core_25_1
Upgrade bitcoin core to v25.1
2023-11-09 01:39:01 +09:00
Mononaut
a33dc74c88 Shake the echarts tree 2023-11-07 23:35:33 +00:00
Erik Arvstedt
939fe951d4 README: add nix-bitcoin to node distros 2023-11-05 13:47:23 +01:00
nymkappa
a9a3623539 Merge pull request #4341 from mempool/nymkappa/hide-accelerate-cta-loading
[accelerator] hide cta while the tx page is loading
2023-11-02 10:21:46 +09:00
nymkappa
27966ad8ec [accelerator] hide cta while the tx page is loading 2023-11-02 10:21:15 +09:00
nymkappa
5d1ebc6d31 Merge pull request #4340 from mempool/nymkappa/fix-search-align-issue
[CSS] fix search align issue when hamburger icon is showing
2023-11-01 17:49:26 +09:00
nymkappa
a68c2a2be6 [css] fix search bar align issue when hamburger icon is showing 2023-11-01 17:47:20 +09:00
Mononaut
83fde600f1 Enforce purging rate minimum on recommended fees 2023-10-25 17:46:04 +00:00
fanquake
26f5776d6d Upgrade bitcoin core to v25.1 2023-10-23 14:44:21 +01:00
fanquake
abee175f4f contrib: add fanquake CLA 2023-10-23 14:44:21 +01:00
Mononaut
6efeeb1d64 Hide mempool count line by default 2023-10-20 15:10:40 +00:00
Boris Nagaev
2bc3352785 [lightning] add taproot-channels to node features
Update from https://github.com/lightningnetwork/lnd/blob/master/lnwire/features.go
2023-10-20 04:49:14 +02:00
Boris Nagaev
50460d4025 contributer license 2023-10-20 04:48:34 +02:00
Felipe Knorr Kuhn
e9e507af69 Fix default config path 2023-10-10 19:53:57 -07:00
Mononaut
af3d6eccfb Fix node group map channel count 2023-10-11 01:09:10 +00:00
Felipe Knorr Kuhn
a35c8be25c Configurable unfurler config 2023-10-08 09:03:22 -07:00
nymkappa
7d3f82eca0 [footer] remove "Sign-up" label 2023-10-05 11:16:01 +02:00
nymkappa
20026f974f Merge remote-tracking branch 'origin/nymkappa/accel-button' into nymkappa/mega-branch 2023-10-04 12:11:10 +02:00
nymkappa
3d964fcdfa Merge branch 'nymkappa/about-page-profile-image' into nymkappa/mega-branch 2023-10-04 12:11:05 +02:00
nymkappa
9e8b2957d0 Merge branch 'nymkappa/menu' into nymkappa/mega-branch 2023-10-04 12:10:58 +02:00
nymkappa
c04e92a686 Merge branch 'nymkappa/accelerate-preview-login-cta' into nymkappa/mega-branch 2023-10-04 12:10:46 +02:00
nymkappa
2dd6dd9233 Merge branch 'nymkappa/polish-accelerate-cta' into nymkappa/mega-branch 2023-10-04 12:10:39 +02:00
nymkappa
e9b72776ed Merge remote-tracking branch 'origin/nymkappa/fix-menu-scroll' into nymkappa/mega-branch 2023-10-04 12:10:28 +02:00
nymkappa
23df1f012c Merge remote-tracking branch 'origin/nymkappa/sticky-header-non-bitcoin' into nymkappa/mega-branch 2023-10-04 12:10:17 +02:00
Mononaut
420fbf3d6f Hide the blockchain arrow when transaction is replaced 2023-09-29 00:02:01 +01:00
Mononaut
6773af92ed Add standalone mempool block visualization page 2023-09-23 23:09:11 +01:00
Mononaut
72750267d0 Enable navigation from standalone block page 2023-09-23 22:26:21 +01:00
Mononaut
80afcae645 Fix faq blockchain positioning 2023-09-23 00:40:47 +01:00
Mononaut
f3fc774c2d Add standalone block visualization page 2023-09-21 21:57:54 +01:00
nymkappa
14a41b3108 [bisq] sticky header 2023-09-15 16:05:10 +02:00
nymkappa
81d1a809d2 [liquid] sticky header 2023-09-15 16:02:39 +02:00
Mononaut
441b505aa3 Send correct tx conf status in websocket msgs 2023-09-14 23:27:40 +00:00
Mononaut
c1f0eac8f8 Add REQUEST_TIMEOUT and FALLBACK_TIMEOUT esplora config options 2023-09-14 22:57:37 +00:00
nymkappa
a000438277 [UI] menu username ellipsis 2023-09-13 09:30:11 +02:00
nymkappa
e3fffd8fb2 [UI] fix menu scroll issue 2023-09-13 09:21:31 +02:00
nymkappa
a133fa8d41 Merge branch 'master' into nymkappa/tx-overflow 2023-09-12 15:12:27 +02:00
nymkappa
5dfd1a495e Merge branch 'master' into nymkappa/about-page-profile-image 2023-09-12 14:34:34 +02:00
nymkappa
a463fc289f Merge branch 'master' into nymkappa/menu 2023-09-12 14:34:28 +02:00
nymkappa
a8e6d9b4b9 Merge branch 'master' into nymkappa/accelerate-preview-login-cta 2023-09-12 14:34:22 +02:00
nymkappa
9ed7e80c44 fix typo post merge 2023-09-12 14:15:29 +02:00
nymkappa
596d55e413 Merge branch 'master' into nymkappa/accel-button 2023-09-12 14:05:56 +02:00
nymkappa
0e1a9d8619 [accelerator] blinking cta, polish accel preview 2023-09-03 16:05:12 +03:00
nymkappa
d09668aaa6 [accelerator] login CTA with redirection 2023-09-03 12:20:30 +03:00
wiz
93aa08b60d Merge branch 'master' into nymkappa/menu 2023-08-31 02:02:50 +09:00
nymkappa
4664e55513 [footer] fix positioning when menu component is not loaded 2023-08-30 18:58:21 +02:00
hunicus
1dd66e6695 Add bimi svg 2023-08-16 12:43:36 +09:00
nymkappa
6199216c54 use new services api to fetch chads profile image as well 2023-08-10 18:08:30 +09:00
nymkappa
b988a4c526 use new services api to fetch chad/whale profile image 2023-08-10 18:01:05 +09:00
nymkappa
89be841e64 [accelerator] hide accelerate button if already accelerating 2023-08-08 17:10:59 +09:00
nymkappa
223f6df371 fix 'large' class trigger 2023-08-05 10:52:11 +09:00
nymkappa
869a879676 Merge branch 'master' into nymkappa/tx-overflow 2023-08-05 10:10:01 +09:00
nymkappa
5959c426f3 Merge branch 'master' into nymkappa/tx-overflow 2023-08-05 10:09:22 +09:00
softsimon
09b7c4021a Merge branch 'master' into nymkappa/tx-overflow 2023-07-19 15:18:23 +09:00
Antoni Spaanderman
0d69ad43ab fix config issues 2023-06-18 20:34:56 +02:00
Antoni Spaanderman
dc491a5984 change default cookie path to /bitcoin/.cookie 2023-06-18 19:16:09 +02:00
Antoni Spaanderman
e9386ec003 Add Bitcoin Core RPC cookie authentication option 2023-06-18 19:16:07 +02:00
nymkappa
6ac1a43d44 Merge branch 'master' into nymkappa/tx-overflow 2023-04-22 20:11:45 +02:00
nymkappa
3db1486bfb Fix transaction amount overflow 2023-03-29 17:32:39 +09:00
217 changed files with 106357 additions and 69388 deletions

View File

@@ -23,6 +23,7 @@ Mempool can be conveniently installed on the following full-node distros:
- [RoninDojo](https://code.samourai.io/ronindojo/RoninDojo)
- [myNode](https://github.com/mynodebtc/mynode)
- [Start9](https://github.com/Start9Labs/embassy-os)
- [nix-bitcoin](https://github.com/fort-nix/nix-bitcoin/blob/a1eacce6768ca4894f365af8f79be5bbd594e1c3/examples/configuration.nix#L129)
**We highly recommend you deploy your own Mempool instance this way.** No matter which option you pick, you'll be able to get your own fully-sovereign instance of Mempool up quickly without needing to fiddle with any settings.

View File

@@ -40,7 +40,9 @@
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool",
"TIMEOUT": 60000
"TIMEOUT": 60000,
"COOKIE": false,
"COOKIE_PATH": "/path/to/bitcoin/.cookie"
},
"ELECTRUM": {
"HOST": "127.0.0.1",
@@ -50,7 +52,10 @@
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:3000",
"UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet",
"BATCH_QUERY_BASE_SIZE": 1000,
"RETRY_UNIX_SOCKET_AFTER": 30000,
"REQUEST_TIMEOUT": 10000,
"FALLBACK_TIMEOUT": 5000,
"FALLBACK": []
},
"SECOND_CORE_RPC": {
@@ -58,7 +63,9 @@
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool",
"TIMEOUT": 60000
"TIMEOUT": 60000,
"COOKIE": false,
"COOKIE_PATH": "/path/to/bitcoin/.cookie"
},
"DATABASE": {
"ENABLED": true,
@@ -126,6 +133,11 @@
"BISQ_URL": "https://bisq.markets/api",
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
},
"REDIS": {
"ENABLED": false,
"UNIX_SOCKET_PATH": "/tmp/redis.sock",
"BATCH_QUERY_BASE_SIZE": 5000
},
"REPLICATION": {
"ENABLED": false,
"AUDIT": false,

View File

@@ -12,7 +12,7 @@
"@babel/core": "^7.23.2",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "~1.5.0",
"axios": "~1.6.1",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0",
"express": "~4.18.2",
@@ -2325,9 +2325,9 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz",
"integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==",
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz",
"integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
@@ -9415,9 +9415,9 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz",
"integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==",
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz",
"integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==",
"requires": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",

View File

@@ -41,7 +41,7 @@
"@babel/core": "^7.23.2",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "~1.5.0",
"axios": "~1.6.1",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0",
"express": "~4.18.2",

View File

@@ -41,7 +41,9 @@
"PORT": 15,
"USERNAME": "__CORE_RPC_USERNAME__",
"PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": 1000
"TIMEOUT": 1000,
"COOKIE": false,
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
},
"ELECTRUM": {
"HOST": "__ELECTRUM_HOST__",
@@ -51,7 +53,10 @@
"ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__",
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
"BATCH_QUERY_BASE_SIZE": 1000,
"RETRY_UNIX_SOCKET_AFTER": 888,
"REQUEST_TIMEOUT": 10000,
"FALLBACK_TIMEOUT": 5000,
"FALLBACK": []
},
"SECOND_CORE_RPC": {
@@ -59,7 +64,9 @@
"PORT": 17,
"USERNAME": "__SECOND_CORE_RPC_USERNAME__",
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__",
"TIMEOUT": 2000
"TIMEOUT": 2000,
"COOKIE": false,
"COOKIE_PATH": "__SECOND_CORE_RPC_COOKIE_PATH__"
},
"DATABASE": {
"ENABLED": false,
@@ -134,6 +141,7 @@
},
"REDIS": {
"ENABLED": false,
"UNIX_SOCKET_PATH": "/tmp/redis.sock"
"UNIX_SOCKET_PATH": "/tmp/redis.sock",
"BATCH_QUERY_BASE_SIZE": 5000
}
}

View File

@@ -55,7 +55,10 @@ describe('Mempool Backend Config', () => {
expect(config.ESPLORA).toStrictEqual({
REST_API_URL: 'http://127.0.0.1:3000',
UNIX_SOCKET_PATH: null,
BATCH_QUERY_BASE_SIZE: 1000,
RETRY_UNIX_SOCKET_AFTER: 30000,
REQUEST_TIMEOUT: 10000,
FALLBACK_TIMEOUT: 5000,
FALLBACK: [],
});
@@ -64,7 +67,9 @@ describe('Mempool Backend Config', () => {
PORT: 8332,
USERNAME: 'mempool',
PASSWORD: 'mempool',
TIMEOUT: 60000
TIMEOUT: 60000,
COOKIE: false,
COOKIE_PATH: '/bitcoin/.cookie'
});
expect(config.SECOND_CORE_RPC).toStrictEqual({
@@ -72,7 +77,9 @@ describe('Mempool Backend Config', () => {
PORT: 8332,
USERNAME: 'mempool',
PASSWORD: 'mempool',
TIMEOUT: 60000
TIMEOUT: 60000,
COOKIE: false,
COOKIE_PATH: '/bitcoin/.cookie'
});
expect(config.DATABASE).toStrictEqual({
@@ -138,7 +145,8 @@ describe('Mempool Backend Config', () => {
expect(config.REDIS).toStrictEqual({
ENABLED: false,
UNIX_SOCKET_PATH: ''
UNIX_SOCKET_PATH: '',
BATCH_QUERY_BASE_SIZE: 5000,
});
});
});

View File

@@ -9,7 +9,7 @@ class Audit {
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
: { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 0, similarity: 1 };
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
}
const matches: string[] = []; // present in both mined block and template
@@ -144,7 +144,12 @@ class Audit {
const numCensored = Object.keys(isCensored).length;
const numMatches = matches.length - 1; // adjust for coinbase tx
const score = numMatches > 0 ? (numMatches / (numMatches + numCensored)) : 0;
let score = 0;
if (numMatches <= 0 && numCensored <= 0) {
score = 1;
} else if (numMatches > 0) {
score = (numMatches / (numMatches + numCensored));
}
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
return {

View File

@@ -3,8 +3,9 @@ import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi {
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
$getRawTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
$getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
$getAllMempoolTransactions(lastTxid: string);
$getAllMempoolTransactions(lastTxid?: string, max_txs?: number);
$getTransactionHex(txId: string): Promise<string>;
$getBlockHeightTip(): Promise<number>;
$getBlockHashTip(): Promise<string>;
@@ -23,6 +24,8 @@ export interface AbstractBitcoinApi {
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
$getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
startHealthChecks(): void;
}
@@ -32,4 +35,5 @@ export interface BitcoinRpcCredentials {
user: string;
pass: string;
timeout: number;
cookie?: string;
}

View File

@@ -60,11 +60,24 @@ class BitcoinApi implements AbstractBitcoinApi {
});
}
async $getRawTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
const txs: IEsploraApi.Transaction[] = [];
for (const txid of txids) {
try {
const tx = await this.$getRawTransaction(txid, false, true);
txs.push(tx);
} catch (err) {
// skip failures
}
}
return txs;
}
$getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getMempoolTransactions not supported by the Bitcoin RPC API.');
}
$getAllMempoolTransactions(lastTxid: string): Promise<IEsploraApi.Transaction[]> {
$getAllMempoolTransactions(lastTxid?: string, max_txs?: number): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getAllMempoolTransactions not supported by the Bitcoin RPC API.');
}
@@ -198,6 +211,19 @@ class BitcoinApi implements AbstractBitcoinApi {
return outspends;
}
async $getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
return this.$getBatchedOutspends(txId);
}
async $getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]> {
const outspends: IEsploraApi.Outspend[] = [];
for (const outpoint of outpoints) {
const outspend = await this.$getOutspend(outpoint.txid, outpoint.vout);
outspends.push(outspend);
}
return outspends;
}
$getEstimatedHashrate(blockHeight: number): Promise<number> {
// 120 is the default block span in Core
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);

View File

@@ -8,6 +8,7 @@ const nodeRpcCredentials: BitcoinRpcCredentials = {
user: config.CORE_RPC.USERNAME,
pass: config.CORE_RPC.PASSWORD,
timeout: config.CORE_RPC.TIMEOUT,
cookie: config.CORE_RPC.COOKIE ? config.CORE_RPC.COOKIE_PATH : undefined,
};
export default new bitcoin.Client(nodeRpcCredentials);

View File

@@ -8,6 +8,7 @@ const nodeRpcCredentials: BitcoinRpcCredentials = {
user: config.SECOND_CORE_RPC.USERNAME,
pass: config.SECOND_CORE_RPC.PASSWORD,
timeout: config.SECOND_CORE_RPC.TIMEOUT,
cookie: config.SECOND_CORE_RPC.COOKIE ? config.SECOND_CORE_RPC.COOKIE_PATH : undefined,
};
export default new bitcoin.Client(nodeRpcCredentials);

View File

@@ -24,7 +24,6 @@ class BitcoinRoutes {
public initRoutes(app: Application) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', this.getTransactionTimes)
.get(config.MEMPOOL.API_URL_PREFIX + 'outspends', this.$getBatchedOutspends)
.get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', this.$getCpfpInfo)
.get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', this.getDifficultyChange)
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', this.getRecommendedFees)
@@ -112,6 +111,7 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends)
.get(config.MEMPOOL.API_URL_PREFIX + 'txs/outspends', this.$getBatchedOutspends)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', this.getBlockHeader)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', this.getBlockTipHash)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/raw', this.getRawBlock)
@@ -174,24 +174,20 @@ class BitcoinRoutes {
res.json(times);
}
private async $getBatchedOutspends(req: Request, res: Response) {
if (!Array.isArray(req.query.txId)) {
res.status(500).send('Not an array');
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
const txids_csv = req.query.txids;
if (!txids_csv || typeof txids_csv !== 'string') {
res.status(500).send('Invalid txids format');
return;
}
if (req.query.txId.length > 50) {
const txids = txids_csv.split(',');
if (txids.length > 50) {
res.status(400).send('Too many txids requested');
return;
}
const txIds: string[] = [];
for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') {
txIds.push(req.query.txId[_txId].toString());
}
}
try {
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txIds);
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
res.json(batchedOutspends);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
@@ -251,7 +247,7 @@ class BitcoinRoutes {
private async getTransaction(req: Request, res: Response) {
try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true, false, false, true);
res.json(transaction);
} catch (e) {
let statusCode = 500;
@@ -577,7 +573,9 @@ class BitcoinRoutes {
}
try {
const addressData = await bitcoinApi.$getScriptHash(req.params.scripthash);
// electrum expects scripthashes in little-endian
const electrumScripthash = req.params.scripthash.match(/../g)?.reverse().join('') ?? '';
const addressData = await bitcoinApi.$getScriptHash(electrumScripthash);
res.json(addressData);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
@@ -594,11 +592,13 @@ class BitcoinRoutes {
}
try {
// electrum expects scripthashes in little-endian
const electrumScripthash = req.params.scripthash.match(/../g)?.reverse().join('') ?? '';
let lastTxId: string = '';
if (req.query.after_txid && typeof req.query.after_txid === 'string') {
lastTxId = req.query.after_txid;
}
const transactions = await bitcoinApi.$getScriptHashTransactions(req.params.scripthash, lastTxId);
const transactions = await bitcoinApi.$getScriptHashTransactions(electrumScripthash, lastTxId);
res.json(transactions);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {

View File

@@ -6,6 +6,7 @@ export namespace IEsploraApi {
size: number;
weight: number;
fee: number;
sigops?: number;
vin: Vin[];
vout: Vout[];
status: Status;

View File

@@ -8,8 +8,9 @@ import logger from '../../logger';
interface FailoverHost {
host: string,
rtts: number[],
rtt: number
rtt: number,
failures: number,
latestHeight?: number,
socket?: boolean,
outOfSync?: boolean,
unreachable?: boolean,
@@ -75,9 +76,9 @@ class FailoverRouter {
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: 5000 });
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: 5000 });
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);
@@ -92,6 +93,7 @@ class FailoverRouter {
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;
} else {
@@ -99,22 +101,23 @@ class FailoverRouter {
}
host.unreachable = false;
} else {
host.outOfSync = true;
host.unreachable = true;
}
}
this.sortHosts();
logger.debug(`Tomahawk ranking: ${this.hosts.map(host => '\navg rtt ' + Math.round(host.rtt).toString().padStart(5, ' ') + ' | reachable? ' + (!host.unreachable || false).toString().padStart(5, ' ') + ' | in sync? ' + (!host.outOfSync || false).toString().padStart(5, ' ') + ` | ${host.host}`).join('')}`);
logger.debug(`Tomahawk ranking:\n${this.hosts.map((host, index) => this.formatRanking(index, host, this.activeHost, maxHeight)).join('\n')}`);
// 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`);
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`);
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`);
logger.debug(`🛠️ ${this.activeHost.host} is no longer the best esplora host 🛠️`);
}
this.electHost();
}
@@ -122,6 +125,11 @@ class FailoverRouter {
this.pollTimer = setTimeout(() => { this.pollHosts(); }, this.pollInterval);
}
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 ? '⭐️' : ' '}`;
}
// sort hosts by connection quality, and update default fallback
private sortHosts(): void {
// sort by connection quality
@@ -156,7 +164,7 @@ class FailoverRouter {
private addFailure(host: FailoverHost): FailoverHost {
host.failures++;
if (host.failures > 5 && this.multihost) {
logger.warn(`Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative`);
logger.warn(`🚨🚨🚨 Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative 🚨🚨🚨`);
this.electHost();
return this.activeHost;
} else {
@@ -168,12 +176,15 @@ class FailoverRouter {
let axiosConfig;
let url;
if (host.socket) {
axiosConfig = { socketPath: host.host, timeout: 10000, responseType };
axiosConfig = { socketPath: host.host, timeout: config.ESPLORA.REQUEST_TIMEOUT, responseType };
url = path;
} else {
axiosConfig = { timeout: 10000, responseType };
axiosConfig = { timeout: config.ESPLORA.REQUEST_TIMEOUT, responseType };
url = host.host + path;
}
if (data?.params) {
axiosConfig.params = data.params;
}
return (method === 'post'
? this.requestConnection.post<T>(url, data, axiosConfig)
: this.requestConnection.get<T>(url, axiosConfig)
@@ -181,7 +192,8 @@ class FailoverRouter {
.catch((e) => {
let fallbackHost = this.fallbackHost;
if (e?.response?.status !== 404) {
logger.warn(`esplora request failed ${e?.response?.status || 500} ${host.host}${path}`);
logger.warn(`esplora request failed ${e?.response?.status} ${host.host}${path}`);
logger.warn(e instanceof Error ? e.message : e);
fallbackHost = this.addFailure(host);
}
if (retry && e?.code === 'ECONNREFUSED' && this.multihost) {
@@ -193,8 +205,8 @@ class FailoverRouter {
});
}
public async $get<T>(path, responseType = 'json'): Promise<T> {
return this.$query<T>('get', path, null, responseType);
public async $get<T>(path, responseType = 'json', params: any = null): Promise<T> {
return this.$query<T>('get', path, params ? { params } : null, responseType);
}
public async $post<T>(path, data: any, responseType = 'json'): Promise<T> {
@@ -213,12 +225,16 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txId);
}
async $getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/mempool/txs', txids, 'json');
async $getRawTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/internal/txs', txids, 'json');
}
async $getAllMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> {
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''));
async $getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/internal/mempool/txs', txids, 'json');
}
async $getAllMempoolTransactions(lastSeenTxid?: string, max_txs?: number): Promise<IEsploraApi.Transaction[]> {
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/internal/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''), 'json', max_txs ? { max_txs } : null);
}
$getTransactionHex(txId: string): Promise<string> {
@@ -238,7 +254,7 @@ class ElectrsApi implements AbstractBitcoinApi {
}
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/block/' + hash + '/txs');
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/internal/block/' + hash + '/txs');
}
$getBlockHash(height: number): Promise<string> {
@@ -290,13 +306,16 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.failoverRouter.$get<IEsploraApi.Outspend[]>('/tx/' + txId + '/outspends');
}
async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
const outspends: IEsploraApi.Outspend[][] = [];
for (const tx of txId) {
const outspend = await this.$getOutspends(tx);
outspends.push(outspend);
}
return outspends;
async $getBatchedOutspends(txids: string[]): Promise<IEsploraApi.Outspend[][]> {
throw new Error('Method not implemented.');
}
async $getBatchedOutspendsInternal(txids: string[]): Promise<IEsploraApi.Outspend[][]> {
return this.failoverRouter.$post<IEsploraApi.Outspend[][]>('/internal/txs/outspends/by-txid', txids, 'json');
}
async $getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]> {
return this.failoverRouter.$post<IEsploraApi.Outspend[]>('/internal/txs/outspends/by-outpoint', outpoints.map(out => `${out.txid}:${out.vout}`), 'json');
}
public startHealthChecks(): void {

View File

@@ -81,6 +81,7 @@ class Blocks {
private async $getTransactionsExtended(
blockHash: string,
blockHeight: number,
blockTime: number,
onlyCoinbase: boolean,
txIds: string[] | null = null,
quiet: boolean = false,
@@ -101,6 +102,12 @@ class Blocks {
if (!onlyCoinbase) {
for (const txid of txIds) {
if (mempool[txid]) {
mempool[txid].status = {
confirmed: true,
block_height: blockHeight,
block_hash: blockHash,
block_time: blockTime,
};
transactionMap[txid] = mempool[txid];
foundInMempool++;
totalFound++;
@@ -608,7 +615,7 @@ class Blocks {
}
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, null, true);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, block.timestamp, true, null, true);
const blockExtended = await this.$getBlockExtended(block, transactions);
newlyIndexed++;
@@ -701,7 +708,7 @@ class Blocks {
const verboseBlock = await bitcoinClient.getBlock(blockHash, 2);
const block = BitcoinApi.convertBlock(verboseBlock);
const txIds: string[] = verboseBlock.tx.map(tx => tx.txid);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false, txIds, false, true) as MempoolTransactionExtended[];
const transactions = await this.$getTransactionsExtended(blockHash, block.height, block.timestamp, false, txIds, false, true) as MempoolTransactionExtended[];
// fill in missing transaction fee data from verboseBlock
for (let i = 0; i < transactions.length; i++) {
@@ -754,8 +761,13 @@ class Blocks {
this.updateTimerProgress(timer, `saved ${this.currentBlockHeight} to database`);
if (!fastForwarded) {
const lastestPriceId = await PricesRepository.$getLatestPriceId();
this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
let lastestPriceId;
try {
lastestPriceId = await PricesRepository.$getLatestPriceId();
this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
} catch (e) {
logger.debug('failed to fetch latest price id from db: ' + (e instanceof Error ? e.message : e));
}
if (priceUpdater.historyInserted === true && lastestPriceId !== null) {
await blocksRepository.$saveBlockPrices([{
height: blockExtended.height,
@@ -764,9 +776,7 @@ class Blocks {
this.updateTimerProgress(timer, `saved prices for ${this.currentBlockHeight}`);
} else {
logger.debug(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining);
setTimeout(() => {
indexer.runSingleTask('blocksPrices');
}, 10000);
indexer.scheduleSingleTask('blocksPrices', 10000);
}
// Save blocks summary for visualization if it's enabled
@@ -890,7 +900,7 @@ class Blocks {
const blockHash = await bitcoinApi.$getBlockHash(height);
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, block.timestamp, true);
const blockExtended = await this.$getBlockExtended(block, transactions);
if (Common.indexingEnabled()) {
@@ -902,7 +912,7 @@ class Blocks {
public async $indexStaleBlock(hash: string): Promise<BlockExtended> {
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(hash);
const transactions = await this.$getTransactionsExtended(hash, block.height, true);
const transactions = await this.$getTransactionsExtended(hash, block.height, block.timestamp, true);
const blockExtended = await this.$getBlockExtended(block, transactions);
blockExtended.canonical = await bitcoinApi.$getBlockHash(block.height);

View File

@@ -252,7 +252,11 @@ class DiskCache {
}
if (rbfData?.rbf) {
rbfCache.load(rbfData.rbf);
rbfCache.load({
txs: rbfData.rbf.txs.map(([txid, entry]) => ({ value: entry })),
trees: rbfData.rbf.trees,
expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })),
});
}
} catch (e) {
logger.warn('Failed to parse rbf cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e));

View File

@@ -3,21 +3,30 @@ import { Common } from './common';
import mempool from './mempool';
import projectedBlocks from './mempool-blocks';
interface RecommendedFees {
fastestFee: number,
halfHourFee: number,
hourFee: number,
economyFee: number,
minimumFee: number,
}
class FeeApi {
constructor() { }
defaultFee = Common.isLiquid() ? 0.1 : 1;
public getRecommendedFee() {
public getRecommendedFee(): RecommendedFees {
const pBlocks = projectedBlocks.getMempoolBlocks();
const mPool = mempool.getMempoolInfo();
const minimumFee = Math.ceil(mPool.mempoolminfee * 100000);
const defaultMinFee = Math.max(minimumFee, this.defaultFee);
if (!pBlocks.length) {
return {
'fastestFee': this.defaultFee,
'halfHourFee': this.defaultFee,
'hourFee': this.defaultFee,
'fastestFee': defaultMinFee,
'halfHourFee': defaultMinFee,
'hourFee': defaultMinFee,
'economyFee': minimumFee,
'minimumFee': minimumFee,
};
@@ -27,11 +36,15 @@ class FeeApi {
const secondMedianFee = pBlocks[1] ? this.optimizeMedianFee(pBlocks[1], pBlocks[2], firstMedianFee) : this.defaultFee;
const thirdMedianFee = pBlocks[2] ? this.optimizeMedianFee(pBlocks[2], pBlocks[3], secondMedianFee) : this.defaultFee;
// explicitly enforce a minimum of ceil(mempoolminfee) on all recommendations.
// simply rounding up recommended rates is insufficient, as the purging rate
// can exceed the median rate of projected blocks in some extreme scenarios
// (see https://bitcoin.stackexchange.com/a/120024)
return {
'fastestFee': firstMedianFee,
'halfHourFee': secondMedianFee,
'hourFee': thirdMedianFee,
'economyFee': Math.min(2 * minimumFee, thirdMedianFee),
'fastestFee': Math.max(minimumFee, firstMedianFee),
'halfHourFee': Math.max(minimumFee, secondMedianFee),
'hourFee': Math.max(minimumFee, thirdMedianFee),
'economyFee': Math.max(minimumFee, Math.min(2 * minimumFee, thirdMedianFee)),
'minimumFee': minimumFee,
};
}

View File

@@ -44,9 +44,13 @@ export enum FeatureBits {
KeysendOptional = 55,
ScriptEnforcedLeaseRequired = 2022,
ScriptEnforcedLeaseOptional = 2023,
SimpleTaprootChannelsRequiredFinal = 80,
SimpleTaprootChannelsOptionalFinal = 81,
SimpleTaprootChannelsRequiredStaging = 180,
SimpleTaprootChannelsOptionalStaging = 181,
MaxBolt11Feature = 5114,
};
export const FeaturesMap = new Map<FeatureBits, string>([
[FeatureBits.DataLossProtectRequired, 'data-loss-protect'],
[FeatureBits.DataLossProtectOptional, 'data-loss-protect'],
@@ -85,6 +89,10 @@ export const FeaturesMap = new Map<FeatureBits, string>([
[FeatureBits.ZeroConfOptional, 'zero-conf'],
[FeatureBits.ShutdownAnySegwitRequired, 'shutdown-any-segwit'],
[FeatureBits.ShutdownAnySegwitOptional, 'shutdown-any-segwit'],
[FeatureBits.SimpleTaprootChannelsRequiredFinal, 'taproot-channels'],
[FeatureBits.SimpleTaprootChannelsOptionalFinal, 'taproot-channels'],
[FeatureBits.SimpleTaprootChannelsRequiredStaging, 'taproot-channels-staging'],
[FeatureBits.SimpleTaprootChannelsOptionalStaging, 'taproot-channels-staging'],
]);
/**

View File

@@ -94,7 +94,7 @@ class Mempool {
logger.debug(`Migrating ${Object.keys(this.mempoolCache).length} transactions from disk cache to Redis cache`);
}
for (const txid of Object.keys(this.mempoolCache)) {
if (!this.mempoolCache[txid].sigops || this.mempoolCache[txid].effectiveFeePerVsize == null) {
if (!this.mempoolCache[txid].adjustedVsize || this.mempoolCache[txid].sigops == null || this.mempoolCache[txid].effectiveFeePerVsize == null) {
this.mempoolCache[txid] = transactionUtils.extendMempoolTransaction(this.mempoolCache[txid]);
}
if (this.mempoolCache[txid].order == null) {
@@ -126,7 +126,7 @@ class Mempool {
loadingIndicators.setProgress('mempool', count / expectedCount * 100);
while (!done) {
try {
const result = await bitcoinApi.$getAllMempoolTransactions(last_txid);
const result = await bitcoinApi.$getAllMempoolTransactions(last_txid, config.ESPLORA.BATCH_QUERY_BASE_SIZE);
if (result) {
for (const tx of result) {
const extendedTransaction = transactionUtils.extendMempoolTransaction(tx);
@@ -235,7 +235,7 @@ class Mempool {
if (!loaded) {
const remainingTxids = transactions.filter(txid => !this.mempoolCache[txid]);
const sliceLength = 10000;
const sliceLength = config.ESPLORA.BATCH_QUERY_BASE_SIZE;
for (let i = 0; i < Math.ceil(remainingTxids.length / sliceLength); i++) {
const slice = remainingTxids.slice(i * sliceLength, (i + 1) * sliceLength);
const txs = await transactionUtils.$getMempoolTransactionsExtended(slice, false, false, false);

View File

@@ -2,6 +2,7 @@ import config from "../config";
import logger from "../logger";
import { MempoolTransactionExtended, TransactionStripped } from "../mempool.interfaces";
import bitcoinApi from './bitcoin/bitcoin-api-factory';
import { IEsploraApi } from "./bitcoin/esplora-api.interface";
import { Common } from "./common";
import redisCache from "./redis-cache";
@@ -53,6 +54,9 @@ class RbfCache {
private expiring: Map<string, number> = new Map();
private cacheQueue: CacheEvent[] = [];
private evictionCount = 0;
private staleCount = 0;
constructor() {
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
}
@@ -245,6 +249,7 @@ class RbfCache {
// flag a transaction as removed from the mempool
public evict(txid: string, fast: boolean = false): void {
this.evictionCount++;
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
this.addExpiration(txid, expiryTime);
@@ -272,18 +277,23 @@ class RbfCache {
this.remove(txid);
}
}
logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.rbfTrees.size} trees, ${this.expiring.size} due to expire`);
logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.rbfTrees.size} trees, ${this.expiring.size} due to expire (${this.evictionCount} newly expired)`);
this.evictionCount = 0;
}
// remove a transaction & all previous versions from the cache
private remove(txid): void {
// don't remove a transaction if a newer version remains in the mempool
if (!this.replacedBy.has(txid)) {
const root = this.treeMap.get(txid);
const replaces = this.replaces.get(txid);
this.replaces.delete(txid);
this.treeMap.delete(txid);
this.removeTx(txid);
this.removeExpiration(txid);
if (root === txid) {
this.removeTree(txid);
}
for (const tx of (replaces || [])) {
// recursively remove prior versions from the cache
this.replacedBy.delete(tx);
@@ -359,18 +369,27 @@ class RbfCache {
}
public async load({ txs, trees, expiring }): Promise<void> {
txs.forEach(txEntry => {
this.txs.set(txEntry.key, txEntry.value);
});
for (const deflatedTree of trees) {
await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
}
expiring.forEach(expiringEntry => {
if (this.txs.has(expiringEntry.key)) {
this.expiring.set(expiringEntry.key, new Date(expiringEntry.value).getTime());
try {
txs.forEach(txEntry => {
this.txs.set(txEntry.value.txid, txEntry.value);
});
this.staleCount = 0;
for (const deflatedTree of trees) {
await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
}
});
this.cleanup();
expiring.forEach(expiringEntry => {
if (this.txs.has(expiringEntry.key)) {
this.expiring.set(expiringEntry.key, new Date(expiringEntry.value).getTime());
}
});
this.staleCount = 0;
await this.checkTrees();
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
this.cleanup();
} catch (e) {
logger.err('failed to restore RBF cache: ' + (e instanceof Error ? e.message : e));
}
}
exportTree(tree: RbfTree, deflated: any = null) {
@@ -398,29 +417,11 @@ class RbfCache {
const treeInfo = deflated[txid];
const replaces: RbfTree[] = [];
// check if any transactions in this tree have already been confirmed
mined = mined || treeInfo.mined;
let exists = mined;
if (!mined) {
try {
const apiTx = await bitcoinApi.$getRawTransaction(txid);
if (apiTx) {
exists = true;
}
if (apiTx?.status?.confirmed) {
mined = true;
treeInfo.txMined = true;
this.evict(txid, true);
}
} catch (e) {
// most transactions do not exist
}
}
// if the root tx is not in the mempool or the blockchain
// evict this tree as soon as possible
if (root === txid && !exists) {
this.evict(txid, true);
// if the root tx is unknown, remove this tree and return early
if (root === txid && !txs.has(txid)) {
this.staleCount++;
this.removeTree(deflated.key);
return;
}
// recursively reconstruct child trees
@@ -458,6 +459,60 @@ class RbfCache {
return tree;
}
private async checkTrees(): Promise<void> {
const found: { [txid: string]: boolean } = {};
const txids = Array.from(this.txs.values()).map(tx => tx.txid).filter(txid => {
return !this.expiring.has(txid) && !this.getRbfTree(txid)?.mined;
});
const processTxs = (txs: IEsploraApi.Transaction[]): void => {
for (const tx of txs) {
found[tx.txid] = true;
if (tx.status?.confirmed) {
const tree = this.getRbfTree(tx.txid);
if (tree) {
this.setTreeMined(tree, tx.txid);
tree.mined = true;
this.evict(tx.txid, false);
}
}
}
};
if (config.MEMPOOL.BACKEND === 'esplora') {
let processedCount = 0;
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 40);
for (let i = 0; i < Math.ceil(txids.length / sliceLength); i++) {
const slice = txids.slice(i * sliceLength, (i + 1) * sliceLength);
processedCount += slice.length;
try {
const txs = await bitcoinApi.$getRawTransactions(slice);
processTxs(txs);
logger.debug(`fetched and processed ${processedCount} of ${txids.length} cached rbf transactions (${(processedCount / txids.length * 100).toFixed(2)}%)`);
} catch (err) {
logger.err(`failed to fetch or process ${slice.length} cached rbf transactions`);
}
}
} else {
const txs: IEsploraApi.Transaction[] = [];
for (const txid of txids) {
try {
const tx = await bitcoinApi.$getRawTransaction(txid, true, false);
txs.push(tx);
} catch (err) {
// some 404s are expected, so continue quietly
}
}
processTxs(txs);
}
for (const txid of txids) {
if (!found[txid]) {
this.evict(txid, false);
}
}
}
public getLatestRbfSummary(): ReplacementInfo[] {
const rbfList = this.getRbfTrees(false);
return rbfList.slice(0, 6).map(rbfTree => {

View File

@@ -122,8 +122,9 @@ class RedisCache {
async $removeTransactions(transactions: string[]) {
try {
await this.$ensureConnected();
for (let i = 0; i < Math.ceil(transactions.length / 10000); i++) {
const slice = transactions.slice(i * 10000, (i + 1) * 10000);
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`);
}
@@ -219,7 +220,7 @@ class RedisCache {
await memPool.$setMempool(loadedMempool);
await rbfCache.load({
txs: rbfTxs,
trees: rbfTrees.map(loadedTree => loadedTree.value),
trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }),
expiring: rbfExpirations,
});
}

View File

@@ -116,7 +116,7 @@ class TransactionUtils {
public extendMempoolTransaction(transaction: IEsploraApi.Transaction): MempoolTransactionExtended {
const vsize = Math.ceil(transaction.weight / 4);
const fractionalVsize = (transaction.weight / 4);
const sigops = !Common.isLiquid() ? this.countSigops(transaction) : 0;
let sigops = Common.isLiquid() ? 0 : (transaction.sigops != null ? transaction.sigops : this.countSigops(transaction));
// https://github.com/bitcoin/bitcoin/blob/e9262ea32a6e1d364fb7974844fadc36f931f8c6/src/policy/policy.cpp#L295-L298
const adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
const feePerVbytes = (transaction.fee || 0) / fractionalVsize;
@@ -155,7 +155,7 @@ class TransactionUtils {
sigops += 20 * (script.match(/OP_CHECKMULTISIG/g)?.length || 0);
} else {
// in redeem scripts and witnesses, worth N if preceded by OP_N, 20 otherwise
const matches = script.matchAll(/(?:OP_(\d+))? OP_CHECKMULTISIG/g);
const matches = script.matchAll(/(?:OP_(?:PUSHNUM_)?(\d+))? OP_CHECKMULTISIG/g);
for (const match of matches) {
const n = parseInt(match[1]);
if (Number.isInteger(n)) {
@@ -189,6 +189,12 @@ class TransactionUtils {
sigops += this.countScriptSigops(bitcoinjs.script.toASM(Buffer.from(input.witness[input.witness.length - 1], 'hex')), false, true);
}
break;
case input.prevout.scriptpubkey_type === 'p2sh':
if (input.inner_redeemscript_asm) {
sigops += this.countScriptSigops(input.inner_redeemscript_asm);
}
break;
}
}
}

View File

@@ -577,7 +577,7 @@ class WebsocketHandler {
response['utxoSpent'] = JSON.stringify(outspends);
}
const rbfReplacedBy = rbfCache.getReplacedBy(client['track-tx']);
const rbfReplacedBy = rbfChanges.map[client['track-tx']] ? rbfCache.getReplacedBy(client['track-tx']) : false;
if (rbfReplacedBy) {
response['rbfTransaction'] = JSON.stringify({
txid: rbfReplacedBy,

View File

@@ -43,7 +43,10 @@ interface IConfig {
ESPLORA: {
REST_API_URL: string;
UNIX_SOCKET_PATH: string | void | null;
BATCH_QUERY_BASE_SIZE: number;
RETRY_UNIX_SOCKET_AFTER: number;
REQUEST_TIMEOUT: number;
FALLBACK_TIMEOUT: number;
FALLBACK: string[];
};
LIGHTNING: {
@@ -76,6 +79,8 @@ interface IConfig {
USERNAME: string;
PASSWORD: string;
TIMEOUT: number;
COOKIE: boolean;
COOKIE_PATH: string;
};
SECOND_CORE_RPC: {
HOST: string;
@@ -83,6 +88,8 @@ interface IConfig {
USERNAME: string;
PASSWORD: string;
TIMEOUT: number;
COOKIE: boolean;
COOKIE_PATH: string;
};
DATABASE: {
ENABLED: boolean;
@@ -145,6 +152,7 @@ interface IConfig {
REDIS: {
ENABLED: boolean;
UNIX_SOCKET_PATH: string;
BATCH_QUERY_BASE_SIZE: number;
},
}
@@ -189,7 +197,10 @@ const defaults: IConfig = {
'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000',
'UNIX_SOCKET_PATH': null,
'BATCH_QUERY_BASE_SIZE': 1000,
'RETRY_UNIX_SOCKET_AFTER': 30000,
'REQUEST_TIMEOUT': 10000,
'FALLBACK_TIMEOUT': 5000,
'FALLBACK': [],
},
'ELECTRUM': {
@@ -203,6 +214,8 @@ const defaults: IConfig = {
'USERNAME': 'mempool',
'PASSWORD': 'mempool',
'TIMEOUT': 60000,
'COOKIE': false,
'COOKIE_PATH': '/bitcoin/.cookie'
},
'SECOND_CORE_RPC': {
'HOST': '127.0.0.1',
@@ -210,6 +223,8 @@ const defaults: IConfig = {
'USERNAME': 'mempool',
'PASSWORD': 'mempool',
'TIMEOUT': 60000,
'COOKIE': false,
'COOKIE_PATH': '/bitcoin/.cookie'
},
'DATABASE': {
'ENABLED': true,
@@ -291,6 +306,7 @@ const defaults: IConfig = {
'REDIS': {
'ENABLED': false,
'UNIX_SOCKET_PATH': '',
'BATCH_QUERY_BASE_SIZE': 5000,
},
};

View File

@@ -2,8 +2,10 @@ import * as fs from 'fs';
import path from 'path';
import config from './config';
import { createPool, Pool, PoolConnection } from 'mysql2/promise';
import { LogLevel } from './logger';
import logger from './logger';
import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } from 'mysql2/typings/mysql';
import { execSync } from 'child_process';
class DB {
constructor() {
@@ -32,7 +34,7 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
}
public async query<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
OkPacket[] | ResultSetHeader>(query, params?, connection?: PoolConnection): Promise<[T, FieldPacket[]]>
OkPacket[] | ResultSetHeader>(query, params?, errorLogLevel: LogLevel | 'silent' = 'debug', connection?: PoolConnection): Promise<[T, FieldPacket[]]>
{
this.checkDBFlag();
let hardTimeout;
@@ -54,19 +56,38 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
}).then(result => {
resolve(result);
}).catch(error => {
if (errorLogLevel !== 'silent') {
logger[errorLogLevel](`database query "${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}" failed!`);
}
reject(error);
}).finally(() => {
clearTimeout(timer);
});
});
} else {
const pool = await this.getPool();
return pool.query(query, params);
try {
const pool = await this.getPool();
return pool.query(query, params);
} catch (e) {
if (errorLogLevel !== 'silent') {
logger[errorLogLevel](`database query "${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}" failed!`);
}
throw e;
}
}
}
private async $rollbackAtomic(connection: PoolConnection): Promise<void> {
try {
await connection.rollback();
await connection.release();
} catch (e) {
logger.warn('Failed to rollback incomplete db transaction: ' + (e instanceof Error ? e.message : e));
}
}
public async $atomicQuery<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
OkPacket[] | ResultSetHeader>(queries: { query, params }[]): Promise<[T, FieldPacket[]][]>
OkPacket[] | ResultSetHeader>(queries: { query, params }[], errorLogLevel: LogLevel | 'silent' = 'debug'): Promise<[T, FieldPacket[]][]>
{
const pool = await this.getPool();
const connection = await pool.getConnection();
@@ -75,7 +96,7 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
const results: [T, FieldPacket[]][] = [];
for (const query of queries) {
const result = await this.query(query.query, query.params, connection) as [T, FieldPacket[]];
const result = await this.query(query.query, query.params, errorLogLevel, connection) as [T, FieldPacket[]];
results.push(result);
}
@@ -83,9 +104,8 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
return results;
} catch (e) {
logger.err('Could not complete db transaction, rolling back: ' + (e instanceof Error ? e.message : e));
connection.rollback();
connection.release();
logger.warn('Could not complete db transaction, rolling back: ' + (e instanceof Error ? e.message : e));
this.$rollbackAtomic(connection);
throw e;
} finally {
connection.release();
@@ -105,26 +125,43 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
public getPidLock(): boolean {
const filePath = path.join(config.DATABASE.PID_DIR || __dirname, `/mempool-${config.DATABASE.DATABASE}.pid`);
this.enforcePidLock(filePath);
fs.writeFileSync(filePath, `${process.pid}`);
return true;
}
private enforcePidLock(filePath: string): void {
if (fs.existsSync(filePath)) {
const pid = fs.readFileSync(filePath).toString();
if (pid !== `${process.pid}`) {
const msg = `Already running on PID ${pid} (or pid file '${filePath}' is stale)`;
const pid = parseInt(fs.readFileSync(filePath, 'utf-8'));
if (pid === process.pid) {
logger.warn('PID file already exists for this process');
return;
}
let cmd;
try {
cmd = execSync(`ps -p ${pid} -o args=`);
} catch (e) {
logger.warn(`Stale PID file at ${filePath}, but no process running on that PID ${pid}`);
return;
}
if (cmd && cmd.toString()?.includes('node')) {
const msg = `Another mempool nodejs process is already running on PID ${pid}`;
logger.err(msg);
throw new Error(msg);
} else {
return true;
logger.warn(`Stale PID file at ${filePath}, but the PID ${pid} does not belong to a running mempool instance`);
}
} else {
fs.writeFileSync(filePath, `${process.pid}`);
return true;
}
}
public releasePidLock(): void {
const filePath = path.join(config.DATABASE.PID_DIR || __dirname, `/mempool-${config.DATABASE.DATABASE}.pid`);
if (fs.existsSync(filePath)) {
const pid = fs.readFileSync(filePath).toString();
if (pid === `${process.pid}`) {
const pid = parseInt(fs.readFileSync(filePath, 'utf-8'));
// only release our own pid file
if (pid === process.pid) {
fs.unlinkSync(filePath);
}
}

View File

@@ -92,9 +92,15 @@ class Server {
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
// Register cleanup listeners for exit events
['exit', 'SIGINT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2', 'uncaughtException', 'unhandledRejection'].forEach(event => {
['exit', 'SIGHUP', 'SIGINT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2'].forEach(event => {
process.on(event, () => { this.onExit(event); });
});
process.on('uncaughtException', (error) => {
this.onUnhandledException('uncaughtException', error);
});
process.on('unhandledRejection', (reason, promise) => {
this.onUnhandledException('unhandledRejection', reason);
});
if (config.MEMPOOL.BACKEND === 'esplora') {
bitcoinApi.startHealthChecks();
@@ -200,7 +206,7 @@ class Server {
}
const newMempool = await bitcoinApi.$getRawMempool();
const numHandledBlocks = await blocks.$updateBlocks();
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerRunning ? 10 : 1);
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
if (numHandledBlocks === 0) {
await memPool.$updateMempool(newMempool, pollRate);
}
@@ -314,14 +320,18 @@ class Server {
}
}
onExit(exitEvent): void {
onExit(exitEvent, code = 0): void {
logger.debug(`onExit for signal: ${exitEvent}`);
if (config.DATABASE.ENABLED) {
DB.releasePidLock();
}
process.exit(0);
process.exit(code);
}
onUnhandledException(type, error): void {
console.error(`${type}:`, error);
this.onExit(type, 1);
}
}
((): Server => new Server())();

View File

@@ -15,11 +15,18 @@ export interface CoreIndex {
best_block_height: number;
}
type TaskName = 'blocksPrices' | 'coinStatsIndex';
class Indexer {
runIndexer = true;
indexerRunning = false;
tasksRunning: string[] = [];
coreIndexes: CoreIndex[] = [];
private runIndexer = true;
private indexerRunning = false;
private tasksRunning: { [key in TaskName]?: boolean; } = {};
private tasksScheduled: { [key in TaskName]?: NodeJS.Timeout; } = {};
private coreIndexes: CoreIndex[] = [];
public indexerIsRunning(): boolean {
return this.indexerRunning;
}
/**
* Check which core index is available for indexing
@@ -69,33 +76,69 @@ class Indexer {
}
}
public async runSingleTask(task: 'blocksPrices' | 'coinStatsIndex'): Promise<void> {
if (!Common.indexingEnabled()) {
return;
}
if (task === 'blocksPrices' && !this.tasksRunning.includes(task) && !['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
this.tasksRunning.push(task);
const lastestPriceId = await PricesRepository.$getLatestPriceId();
if (priceUpdater.historyInserted === false || lastestPriceId === null) {
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining);
setTimeout(() => {
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
this.runSingleTask('blocksPrices');
}, 10000);
} else {
logger.debug(`Blocks prices indexer will run now`, logger.tags.mining);
await mining.$indexBlockPrices();
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
/**
* schedules a single task to run in `timeout` ms
* only one task of each type may be scheduled
*
* @param {TaskName} task - the type of task
* @param {number} timeout - delay in ms
* @param {boolean} replace - `true` replaces any already scheduled task (works like a debounce), `false` ignores subsequent requests (works like a throttle)
*/
public scheduleSingleTask(task: TaskName, timeout: number = 10000, replace = false): void {
if (this.tasksScheduled[task]) {
if (!replace) { //throttle
return;
} else { // debounce
clearTimeout(this.tasksScheduled[task]);
}
}
this.tasksScheduled[task] = setTimeout(async () => {
try {
await this.runSingleTask(task);
} catch (e) {
logger.err(`Unexpected error in scheduled task ${task}: ` + (e instanceof Error ? e.message : e));
} finally {
clearTimeout(this.tasksScheduled[task]);
}
}, timeout);
}
if (task === 'coinStatsIndex' && !this.tasksRunning.includes(task)) {
this.tasksRunning.push(task);
logger.debug(`Indexing coinStatsIndex now`);
await mining.$indexCoinStatsIndex();
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
/**
* Runs a single task immediately
*
* (use `scheduleSingleTask` instead to queue a task to run after some timeout)
*/
public async runSingleTask(task: TaskName): Promise<void> {
if (!Common.indexingEnabled() || this.tasksRunning[task]) {
return;
}
this.tasksRunning[task] = true;
switch (task) {
case 'blocksPrices': {
if (!['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
let lastestPriceId;
try {
lastestPriceId = await PricesRepository.$getLatestPriceId();
} catch (e) {
logger.debug('failed to fetch latest price id from db: ' + (e instanceof Error ? e.message : e));
} if (priceUpdater.historyInserted === false || lastestPriceId === null) {
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining);
this.scheduleSingleTask(task, 10000);
} else {
logger.debug(`Blocks prices indexer will run now`, logger.tags.mining);
await mining.$indexBlockPrices();
}
}
} break;
case 'coinStatsIndex': {
logger.debug(`Indexing coinStatsIndex now`);
await mining.$indexCoinStatsIndex();
} break;
}
this.tasksRunning[task] = false;
}
public async $run(): Promise<void> {

View File

@@ -157,4 +157,6 @@ class Logger {
}
}
export type LogLevel = 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
export default new Logger();

View File

@@ -14,7 +14,7 @@ class NodesSocketsRepository {
await DB.query(`
INSERT INTO nodes_sockets(public_key, socket, type)
VALUE (?, ?, ?)
`, [socket.publicKey, socket.addr, socket.network]);
`, [socket.publicKey, socket.addr, socket.network], 'silent');
} catch (e: any) {
if (e.errno !== 1062) { // ER_DUP_ENTRY - Not an issue, just ignore this
logger.err(`Cannot save node socket (${[socket.publicKey, socket.addr, socket.network]}) into db. Reason: ` + (e instanceof Error ? e.message : e));

View File

@@ -1,5 +1,6 @@
var http = require('http')
var https = require('https')
import { readFileSync } from 'fs';
var JsonRPC = function (opts) {
// @ts-ignore
@@ -55,7 +56,13 @@ JsonRPC.prototype.call = function (method, params) {
}
// use HTTP auth if user and password set
if (this.opts.user && this.opts.pass) {
if (this.opts.cookie) {
if (!this.cachedCookie) {
this.cachedCookie = readFileSync(this.opts.cookie).toString();
}
// @ts-ignore
requestOptions.auth = this.cachedCookie;
} else if (this.opts.user && this.opts.pass) {
// @ts-ignore
requestOptions.auth = this.opts.user + ':' + this.opts.pass
}
@@ -93,7 +100,7 @@ JsonRPC.prototype.call = function (method, params) {
reject(err)
})
request.on('response', function (response) {
request.on('response', (response) => {
clearTimeout(reqTimeout)
// We need to buffer the response chunks in a nonblocking way.
@@ -104,7 +111,7 @@ JsonRPC.prototype.call = function (method, params) {
// When all the responses are finished, we decode the JSON and
// depending on whether it's got a result or an error, we call
// emitSuccess or emitError on the promise.
response.on('end', function () {
response.on('end', () => {
var err
if (cbCalled) return
@@ -113,6 +120,14 @@ JsonRPC.prototype.call = function (method, params) {
try {
var decoded = JSON.parse(buffer)
} catch (e) {
// if we authenticated using a cookie and it failed, read the cookie file again
if (
response.statusCode === 401 /* Unauthorized */ &&
this.opts.cookie
) {
this.cachedCookie = undefined;
}
if (response.statusCode !== 200) {
err = new Error('Invalid params, response status code: ' + response.statusCode)
err.code = -32602

View File

@@ -15,8 +15,6 @@ class ForensicsService {
txCache: { [txid: string]: IEsploraApi.Transaction } = {};
tempCached: string[] = [];
constructor() {}
public async $startService(): Promise<void> {
logger.info('Starting lightning network forensics service');
@@ -66,93 +64,138 @@ class ForensicsService {
*/
public async $runClosedChannelsForensics(onlyNewChannels: boolean = false): Promise<void> {
// Only Esplora backend can retrieve spent transaction outputs
if (config.MEMPOOL.BACKEND !== 'esplora') {
return;
}
let progress = 0;
try {
logger.debug(`Started running closed channel forensics...`);
let channels;
let allChannels;
if (onlyNewChannels) {
channels = await channelsApi.$getClosedChannelsWithoutReason();
allChannels = await channelsApi.$getClosedChannelsWithoutReason();
} else {
channels = await channelsApi.$getUnresolvedClosedChannels();
allChannels = await channelsApi.$getUnresolvedClosedChannels();
}
for (const channel of channels) {
let reason = 0;
let resolvedForceClose = false;
// Only Esplora backend can retrieve spent transaction outputs
const cached: string[] = [];
let progress = 0;
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 10);
// process batches of 1000 channels
for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) {
const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength);
let allOutspends: IEsploraApi.Outspend[][] = [];
const forceClosedChannels: { channel: any, cachedSpends: string[] }[] = [];
// fetch outspends in bulk
try {
let outspends: IEsploraApi.Outspend[] | undefined;
try {
outspends = await bitcoinApi.$getOutspends(channel.closing_transaction_id);
await Common.sleep$(config.LIGHTNING.FORENSICS_RATE_LIMIT);
} catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + channel.closing_transaction_id + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
continue;
}
const lightningScriptReasons: number[] = [];
const outspendTxids = channels.map(channel => channel.closing_transaction_id);
allOutspends = await bitcoinApi.$getBatchedOutspendsInternal(outspendTxids);
logger.info(`Fetched outspends for ${allOutspends.length} txs from esplora for LN forensics`);
await Common.sleep$(config.LIGHTNING.FORENSICS_RATE_LIMIT);
} catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/internal/txs/outspends/by-txid'}. Reason ${e instanceof Error ? e.message : e}`);
}
// fetch spending transactions in bulk and load into txCache
const newSpendingTxids: { [txid: string]: boolean } = {};
for (const outspends of allOutspends) {
for (const outspend of outspends) {
if (outspend.spent && outspend.txid) {
let spendingTx = await this.fetchTransaction(outspend.txid);
if (!spendingTx) {
continue;
}
cached.push(spendingTx.txid);
const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
lightningScriptReasons.push(lightningScript);
newSpendingTxids[outspend.txid] = true;
}
}
const filteredReasons = lightningScriptReasons.filter((r) => r !== 1);
if (filteredReasons.length) {
if (filteredReasons.some((r) => r === 2 || r === 4)) {
reason = 3;
} else {
reason = 2;
resolvedForceClose = true;
}
} else {
/*
We can detect a commitment transaction (force close) by reading Sequence and Locktime
https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction
*/
let closingTx = await this.fetchTransaction(channel.closing_transaction_id, true);
if (!closingTx) {
}
const allOutspendTxs = await this.fetchTransactions(
allOutspends.flatMap(outspends =>
outspends
.filter(outspend => outspend.spent && outspend.txid)
.map(outspend => outspend.txid)
)
);
logger.info(`Fetched ${allOutspendTxs.length} out-spending txs from esplora for LN forensics`);
// process each outspend
for (const [index, channel] of channels.entries()) {
let reason = 0;
const cached: string[] = [];
try {
const outspends = allOutspends[index];
if (!outspends || !outspends.length) {
// outspends are missing
continue;
}
cached.push(closingTx.txid);
const sequenceHex: string = closingTx.vin[0].sequence.toString(16);
const locktimeHex: string = closingTx.locktime.toString(16);
if (sequenceHex.substring(0, 2) === '80' && locktimeHex.substring(0, 2) === '20') {
reason = 2; // Here we can't be sure if it's a penalty or not
} else {
reason = 1;
const lightningScriptReasons: number[] = [];
for (const outspend of outspends) {
if (outspend.spent && outspend.txid) {
const spendingTx = this.txCache[outspend.txid];
if (!spendingTx) {
continue;
}
cached.push(spendingTx.txid);
const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
lightningScriptReasons.push(lightningScript);
}
}
}
if (reason) {
logger.debug('Setting closing reason ' + reason + ' for channel: ' + channel.id + '.');
await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
if (reason === 2 && resolvedForceClose) {
await DB.query(`UPDATE channels SET closing_resolved = ? WHERE id = ?`, [true, channel.id]);
}
if (reason !== 2 || resolvedForceClose) {
const filteredReasons = lightningScriptReasons.filter((r) => r !== 1);
if (filteredReasons.length) {
if (filteredReasons.some((r) => r === 2 || r === 4)) {
// Force closed with penalty
reason = 3;
} else {
// Force closed without penalty
reason = 2;
await DB.query(`UPDATE channels SET closing_resolved = ? WHERE id = ?`, [true, channel.id]);
}
await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
// clean up cached transactions
cached.forEach(txid => {
delete this.txCache[txid];
});
} else {
forceClosedChannels.push({ channel, cachedSpends: cached });
}
} catch (e) {
logger.err(`$runClosedChannelsForensics() failed for channel ${channel.short_id}. Reason: ${e instanceof Error ? e.message : e}`);
}
} catch (e) {
logger.err(`$runClosedChannelsForensics() failed for channel ${channel.short_id}. Reason: ${e instanceof Error ? e.message : e}`);
}
++progress;
// fetch force-closing transactions in bulk
const closingTxs = await this.fetchTransactions(forceClosedChannels.map(x => x.channel.closing_transaction_id));
logger.info(`Fetched ${closingTxs.length} closing txs from esplora for LN forensics`);
// process channels with no lightning script reasons
for (const { channel, cachedSpends } of forceClosedChannels) {
const closingTx = this.txCache[channel.closing_transaction_id];
if (!closingTx) {
// no channel close transaction found yet
continue;
}
/*
We can detect a commitment transaction (force close) by reading Sequence and Locktime
https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction
*/
const sequenceHex: string = closingTx.vin[0].sequence.toString(16);
const locktimeHex: string = closingTx.locktime.toString(16);
let reason;
if (sequenceHex.substring(0, 2) === '80' && locktimeHex.substring(0, 2) === '20') {
// Force closed, but we can't be sure if it's a penalty or not
reason = 2;
} else {
// Mutually closed
reason = 1;
// clean up cached transactions
delete this.txCache[closingTx.txid];
for (const txid of cachedSpends) {
delete this.txCache[txid];
}
}
await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
}
progress += channels.length;
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
if (elapsedSeconds > 10) {
logger.debug(`Updating channel closed channel forensics ${progress}/${channels.length}`);
logger.debug(`Updating channel closed channel forensics ${progress}/${allChannels.length}`);
this.loggerTimer = new Date().getTime() / 1000;
}
}
@@ -220,8 +263,11 @@ class ForensicsService {
logger.debug(`Started running open channel forensics...`);
const channels = await channelsApi.$getChannelsWithoutSourceChecked();
// preload open channel transactions
await this.fetchTransactions(channels.map(channel => channel.transaction_id), true);
for (const openChannel of channels) {
let openTx = await this.fetchTransaction(openChannel.transaction_id, true);
const openTx = this.txCache[openChannel.transaction_id];
if (!openTx) {
continue;
}
@@ -276,7 +322,7 @@ class ForensicsService {
// Check if a channel open tx input spends the result of a swept channel close output
private async $attributeSweptChannelCloses(openChannel: ILightningApi.Channel, input: IEsploraApi.Vin): Promise<void> {
let sweepTx = await this.fetchTransaction(input.txid, true);
const sweepTx = await this.fetchTransaction(input.txid, true);
if (!sweepTx) {
logger.err(`couldn't find input transaction for channel forensics ${openChannel.channel_id} ${input.txid}`);
return;
@@ -335,7 +381,7 @@ class ForensicsService {
if (matched && !ambiguous) {
// fetch closing channel transaction and perform forensics on the outputs
let prevChannelTx = await this.fetchTransaction(input.txid, true);
const prevChannelTx = await this.fetchTransaction(input.txid, true);
let outspends: IEsploraApi.Outspend[] | undefined;
try {
outspends = await bitcoinApi.$getOutspends(input.txid);
@@ -355,17 +401,17 @@ class ForensicsService {
};
});
}
// preload outspend transactions
await this.fetchTransactions(outspends.filter(o => o.spent && o.txid).map(o => o.txid), true);
for (let i = 0; i < outspends?.length; i++) {
const outspend = outspends[i];
const output = prevChannel.outputs[i];
if (outspend.spent && outspend.txid) {
try {
const spendingTx = await this.fetchTransaction(outspend.txid, true);
if (spendingTx) {
output.type = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
}
} catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + outspend.txid}. Reason ${e instanceof Error ? e.message : e}`);
const spendingTx = this.txCache[outspend.txid];
if (spendingTx) {
output.type = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
}
} else {
output.type = 0;
@@ -430,13 +476,36 @@ class ForensicsService {
}
await Common.sleep$(config.LIGHTNING.FORENSICS_RATE_LIMIT);
} catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + txid + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + txid}. Reason ${e instanceof Error ? e.message : e}`);
return null;
}
}
return tx;
}
// fetches a batch of transactions and adds them to the txCache
// the returned list of txs does *not* preserve ordering or number
async fetchTransactions(txids, temp: boolean = false): Promise<(IEsploraApi.Transaction | null)[]> {
// deduplicate txids
const uniqueTxids = [...new Set<string>(txids)];
// filter out any transactions we already have in the cache
const needToFetch: string[] = uniqueTxids.filter(txid => !this.txCache[txid]);
try {
const txs = await bitcoinApi.$getRawTransactions(needToFetch);
for (const tx of txs) {
this.txCache[tx.txid] = tx;
if (temp) {
this.tempCached.push(tx.txid);
}
}
await Common.sleep$(config.LIGHTNING.FORENSICS_RATE_LIMIT);
} catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/txs'}. Reason ${e instanceof Error ? e.message : e}`);
return [];
}
return txids.map(txid => this.txCache[txid]);
}
clearTempCache(): void {
for (const txid of this.tempCached) {
delete this.txCache[txid];

View File

@@ -288,22 +288,32 @@ class NetworkSyncService {
}
logger.debug(`${log}`, logger.tags.ln);
const channels = await channelsApi.$getChannelsByStatus([0, 1]);
for (const channel of channels) {
const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout);
if (spendingTx.spent === true && spendingTx.status?.confirmed === true) {
logger.debug(`Marking channel: ${channel.id} as closed.`, logger.tags.ln);
await DB.query(`UPDATE channels SET status = 2, closing_date = FROM_UNIXTIME(?) WHERE id = ?`,
[spendingTx.status.block_time, channel.id]);
if (spendingTx.txid && !channel.closing_transaction_id) {
await DB.query(`UPDATE channels SET closing_transaction_id = ? WHERE id = ?`, [spendingTx.txid, channel.id]);
const allChannels = await channelsApi.$getChannelsByStatus([0, 1]);
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 2);
// process batches of 5000 channels
for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) {
const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength);
const outspends = await bitcoinApi.$getOutSpendsByOutpoint(channels.map(channel => {
return { txid: channel.transaction_id, vout: channel.transaction_vout };
}));
for (const [index, channel] of channels.entries()) {
const spendingTx = outspends[index];
if (spendingTx.spent === true && spendingTx.status?.confirmed === true) {
// logger.debug(`Marking channel: ${channel.id} as closed.`, logger.tags.ln);
await DB.query(`UPDATE channels SET status = 2, closing_date = FROM_UNIXTIME(?) WHERE id = ?`,
[spendingTx.status.block_time, channel.id]);
if (spendingTx.txid && !channel.closing_transaction_id) {
await DB.query(`UPDATE channels SET closing_transaction_id = ? WHERE id = ?`, [spendingTx.txid, channel.id]);
}
}
}
++progress;
progress += channels.length;
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) {
logger.debug(`Checking if channel has been closed ${progress}/${channels.length}`, logger.tags.ln);
logger.debug(`Checking if channel has been closed ${progress}/${allChannels.length}`, logger.tags.ln);
this.loggerTimer = new Date().getTime() / 1000;
}
}

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 October 23, 2023.
Signed: fanquake

3
contributors/ncois.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 November 16, 2023.
Signed: ncois

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 November 15, 2023.
Signed: shubhamkmr04

3
contributors/starius.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 Oct 13, 2023.
Signed starius

View File

@@ -164,7 +164,9 @@ Corresponding `docker-compose.yml` overrides:
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool",
"TIMEOUT": 60000
"TIMEOUT": 60000,
"COOKIE": false,
"COOKIE_PATH": ""
},
```
@@ -177,6 +179,8 @@ Corresponding `docker-compose.yml` overrides:
CORE_RPC_USERNAME: ""
CORE_RPC_PASSWORD: ""
CORE_RPC_TIMEOUT: 60000
CORE_RPC_COOKIE: false
CORE_RPC_COOKIE_PATH: ""
...
```
@@ -231,7 +235,9 @@ Corresponding `docker-compose.yml` overrides:
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool",
"TIMEOUT": 60000
"TIMEOUT": 60000,
"COOKIE": false,
"COOKIE_PATH": ""
},
```
@@ -244,6 +250,8 @@ Corresponding `docker-compose.yml` overrides:
SECOND_CORE_RPC_USERNAME: ""
SECOND_CORE_RPC_PASSWORD: ""
SECOND_CORE_RPC_TIMEOUT: ""
SECOND_CORE_RPC_COOKIE: false
SECOND_CORE_RPC_COOKIE_PATH: ""
...
```

View File

@@ -41,7 +41,9 @@
"PORT": __CORE_RPC_PORT__,
"USERNAME": "__CORE_RPC_USERNAME__",
"PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": __CORE_RPC_TIMEOUT__
"TIMEOUT": __CORE_RPC_TIMEOUT__,
"COOKIE": __CORE_RPC_COOKIE__,
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
},
"ELECTRUM": {
"HOST": "__ELECTRUM_HOST__",
@@ -51,7 +53,10 @@
"ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__",
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
"BATCH_QUERY_BASE_SIZE": __ESPLORA_BATCH_QUERY_BASE_SIZE__,
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__,
"REQUEST_TIMEOUT": __ESPLORA_REQUEST_TIMEOUT__,
"FALLBACK_TIMEOUT": __ESPLORA_FALLBACK_TIMEOUT__,
"FALLBACK": __ESPLORA_FALLBACK__
},
"SECOND_CORE_RPC": {
@@ -59,7 +64,9 @@
"PORT": __SECOND_CORE_RPC_PORT__,
"USERNAME": "__SECOND_CORE_RPC_USERNAME__",
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__",
"TIMEOUT": __SECOND_CORE_RPC_TIMEOUT__
"TIMEOUT": __SECOND_CORE_RPC_TIMEOUT__,
"COOKIE": __SECOND_CORE_RPC_COOKIE__,
"COOKIE_PATH": "__SECOND_CORE_RPC_COOKIE_PATH__"
},
"DATABASE": {
"ENABLED": __DATABASE_ENABLED__,
@@ -140,6 +147,7 @@
},
"REDIS": {
"ENABLED": __REDIS_ENABLED__,
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__"
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",
"BATCH_QUERY_BASE_SIZE": __REDIS_BATCH_QUERY_BASE_SIZE__
}
}

View File

@@ -43,6 +43,8 @@ __CORE_RPC_PORT__=${CORE_RPC_PORT:=8332}
__CORE_RPC_USERNAME__=${CORE_RPC_USERNAME:=mempool}
__CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool}
__CORE_RPC_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000}
__CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false}
__CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""}
# ELECTRUM
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1}
@@ -52,7 +54,10 @@ __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_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}
__ESPLORA_FALLBACK_TIMEOUT__=${ESPLORA_FALLBACK_TIMEOUT:=5000}
__ESPLORA_FALLBACK__=${ESPLORA_FALLBACK:=[]}
# SECOND_CORE_RPC
@@ -61,6 +66,8 @@ __SECOND_CORE_RPC_PORT__=${SECOND_CORE_RPC_PORT:=8332}
__SECOND_CORE_RPC_USERNAME__=${SECOND_CORE_RPC_USERNAME:=mempool}
__SECOND_CORE_RPC_PASSWORD__=${SECOND_CORE_RPC_PASSWORD:=mempool}
__SECOND_CORE_RPC_TIMEOUT__=${SECOND_CORE_RPC_TIMEOUT:=60000}
__SECOND_CORE_RPC_COOKIE__=${SECOND_CORE_RPC_COOKIE:=false}
__SECOND_CORE_RPC_COOKIE_PATH__=${SECOND_CORE_RPC_COOKIE_PATH:=""}
# DATABASE
__DATABASE_ENABLED__=${DATABASE_ENABLED:=true}
@@ -142,6 +149,7 @@ __MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
__REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000}
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
@@ -186,6 +194,8 @@ sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json
sed -i "s!__CORE_RPC_USERNAME__!${__CORE_RPC_USERNAME__}!g" mempool-config.json
sed -i "s!__CORE_RPC_PASSWORD__!${__CORE_RPC_PASSWORD__}!g" mempool-config.json
sed -i "s!__CORE_RPC_TIMEOUT__!${__CORE_RPC_TIMEOUT__}!g" mempool-config.json
sed -i "s!__CORE_RPC_COOKIE__!${__CORE_RPC_COOKIE__}!g" mempool-config.json
sed -i "s!__CORE_RPC_COOKIE_PATH__!${__CORE_RPC_COOKIE_PATH__}!g" mempool-config.json
sed -i "s!__ELECTRUM_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json
sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!g" mempool-config.json
@@ -193,7 +203,10 @@ sed -i "s!__ELECTRUM_TLS_ENABLED__!${__ELECTRUM_TLS_ENABLED__}!g" mempool-config
sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json
sed -i "s!__ESPLORA_UNIX_SOCKET_PATH__!${__ESPLORA_UNIX_SOCKET_PATH__}!g" mempool-config.json
sed -i "s!__ESPLORA_BATCH_QUERY_BASE_SIZE__!${__ESPLORA_BATCH_QUERY_BASE_SIZE__}!g" mempool-config.json
sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTER__}!g" mempool-config.json
sed -i "s!__ESPLORA_REQUEST_TIMEOUT__!${__ESPLORA_REQUEST_TIMEOUT__}!g" mempool-config.json
sed -i "s!__ESPLORA_FALLBACK_TIMEOUT__!${__ESPLORA_FALLBACK_TIMEOUT__}!g" mempool-config.json
sed -i "s!__ESPLORA_FALLBACK__!${__ESPLORA_FALLBACK__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_HOST__!${__SECOND_CORE_RPC_HOST__}!g" mempool-config.json
@@ -201,6 +214,8 @@ sed -i "s!__SECOND_CORE_RPC_PORT__!${__SECOND_CORE_RPC_PORT__}!g" mempool-config
sed -i "s!__SECOND_CORE_RPC_USERNAME__!${__SECOND_CORE_RPC_USERNAME__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_PASSWORD__!${__SECOND_CORE_RPC_PASSWORD__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_TIMEOUT__!${__SECOND_CORE_RPC_TIMEOUT__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_COOKIE__!${__SECOND_CORE_RPC_COOKIE__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_COOKIE_PATH__!${__SECOND_CORE_RPC_COOKIE_PATH__}!g" mempool-config.json
sed -i "s!__DATABASE_ENABLED__!${__DATABASE_ENABLED__}!g" mempool-config.json
sed -i "s!__DATABASE_HOST__!${__DATABASE_HOST__}!g" mempool-config.json
@@ -276,5 +291,6 @@ sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS_
# REDIS
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
sed -i "s!__REDIS_BATCH_QUERY_BASE_SIZE__!${__REDIS_BATCH_QUERY_BASE_SIZE__}!g" mempool-config.json
node /backend/package/index.js

View File

@@ -31,11 +31,11 @@
"bootstrap": "~4.6.2",
"browserify": "^17.0.0",
"clipboard": "^2.0.11",
"cypress": "^13.6.0",
"domino": "^2.1.6",
"echarts": "~5.4.3",
"echarts-gl": "^2.0.9",
"lightweight-charts": "~3.8.0",
"ngx-echarts": "~16.0.0",
"ngx-echarts": "~16.2.0",
"ngx-infinite-scroll": "^16.0.0",
"qrcode": "1.5.1",
"rxjs": "~7.8.1",
@@ -59,10 +59,10 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.3.0",
"cypress": "^13.6.0",
"cypress-fail-on-console-error": "~5.0.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.2.1",
"mock-socket": "~9.3.1",
"start-server-and-test": "~2.0.0"
}
},
@@ -6429,11 +6429,6 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/claygl": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/claygl/-/claygl-1.3.0.tgz",
"integrity": "sha512-+gGtJjT6SSHD2l2yC3MCubW/sCV40tZuSs5opdtn79vFSGUgp/lH139RNEQ6Jy078/L0aV8odCw8RSrUcMfLaQ=="
},
"node_modules/clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -7153,9 +7148,9 @@
"peer": true
},
"node_modules/cypress": {
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.3.0.tgz",
"integrity": "sha512-mpI8qcTwLGiA4zEQvTC/U1xGUezVV4V8HQCOYjlEOrVmU1etVvxOjkCXHGwrlYdZU/EPmUiWfsO3yt1o+Q2bgw==",
"version": "13.6.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz",
"integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
@@ -7825,18 +7820,6 @@
"zrender": "5.4.4"
}
},
"node_modules/echarts-gl": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/echarts-gl/-/echarts-gl-2.0.9.tgz",
"integrity": "sha512-oKeMdkkkpJGWOzjgZUsF41DOh6cMsyrGGXimbjK2l6Xeq/dBQu4ShG2w2Dzrs/1bD27b2pLTGSaUzouY191gzA==",
"dependencies": {
"claygl": "^1.2.1",
"zrender": "^5.1.1"
},
"peerDependencies": {
"echarts": "^5.1.2"
}
},
"node_modules/echarts/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
@@ -9531,9 +9514,9 @@
}
},
"node_modules/get-func-name": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
"optional": true,
"engines": {
"node": "*"
@@ -12209,9 +12192,9 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
},
"node_modules/mock-socket": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.2.1.tgz",
"integrity": "sha512-aw9F9T9G2zpGipLLhSNh6ZpgUyUl4frcVmRN08uE1NWPWg43Wx6+sGPDbQ7E5iFZZDJW5b5bypMeAEHqTbIFag==",
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz",
"integrity": "sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==",
"optional": true,
"engines": {
"node": ">= 8"
@@ -12460,9 +12443,9 @@
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
},
"node_modules/ngx-echarts": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-16.0.0.tgz",
"integrity": "sha512-hdM7/CL29bY3sF3V5ihb7H1NeUsQlhijp8tVxT23+vkNTf9SJrUHPjs9oHOMkbTlr2Q8HB+eVpckYAL/tuK0CQ==",
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-16.2.0.tgz",
"integrity": "sha512-yhuDbp6qdkmR4kRVLS06Z0Iumod7xOj5n/Z++clRiKM24OQ4sM8WuOTicdfWy6eeYDNywdGSrri4Y5SUGRD8bg==",
"dependencies": {
"tslib": "^2.3.0"
},
@@ -21550,11 +21533,6 @@
"safe-buffer": "^5.0.1"
}
},
"claygl": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/claygl/-/claygl-1.3.0.tgz",
"integrity": "sha512-+gGtJjT6SSHD2l2yC3MCubW/sCV40tZuSs5opdtn79vFSGUgp/lH139RNEQ6Jy078/L0aV8odCw8RSrUcMfLaQ=="
},
"clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -22118,9 +22096,9 @@
"peer": true
},
"cypress": {
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.3.0.tgz",
"integrity": "sha512-mpI8qcTwLGiA4zEQvTC/U1xGUezVV4V8HQCOYjlEOrVmU1etVvxOjkCXHGwrlYdZU/EPmUiWfsO3yt1o+Q2bgw==",
"version": "13.6.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz",
"integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==",
"optional": true,
"requires": {
"@cypress/request": "^3.0.0",
@@ -22659,15 +22637,6 @@
}
}
},
"echarts-gl": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/echarts-gl/-/echarts-gl-2.0.9.tgz",
"integrity": "sha512-oKeMdkkkpJGWOzjgZUsF41DOh6cMsyrGGXimbjK2l6Xeq/dBQu4ShG2w2Dzrs/1bD27b2pLTGSaUzouY191gzA==",
"requires": {
"claygl": "^1.2.1",
"zrender": "^5.1.1"
}
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -23971,9 +23940,9 @@
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
},
"get-func-name": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
"optional": true
},
"get-intrinsic": {
@@ -25945,9 +25914,9 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
},
"mock-socket": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.2.1.tgz",
"integrity": "sha512-aw9F9T9G2zpGipLLhSNh6ZpgUyUl4frcVmRN08uE1NWPWg43Wx6+sGPDbQ7E5iFZZDJW5b5bypMeAEHqTbIFag==",
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz",
"integrity": "sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==",
"optional": true
},
"module-deps": {
@@ -26141,9 +26110,9 @@
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
},
"ngx-echarts": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-16.0.0.tgz",
"integrity": "sha512-hdM7/CL29bY3sF3V5ihb7H1NeUsQlhijp8tVxT23+vkNTf9SJrUHPjs9oHOMkbTlr2Q8HB+eVpckYAL/tuK0CQ==",
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-16.2.0.tgz",
"integrity": "sha512-yhuDbp6qdkmR4kRVLS06Z0Iumod7xOj5n/Z++clRiKM24OQ4sM8WuOTicdfWy6eeYDNywdGSrri4Y5SUGRD8bg==",
"requires": {
"tslib": "^2.3.0"
}

View File

@@ -85,9 +85,8 @@
"clipboard": "^2.0.11",
"domino": "^2.1.6",
"echarts": "~5.4.3",
"echarts-gl": "^2.0.9",
"lightweight-charts": "~3.8.0",
"ngx-echarts": "~16.0.0",
"ngx-echarts": "~16.2.0",
"ngx-infinite-scroll": "^16.0.0",
"qrcode": "1.5.1",
"rxjs": "~7.8.1",
@@ -111,10 +110,10 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.3.0",
"cypress": "^13.6.0",
"cypress-fail-on-console-error": "~5.0.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.2.1",
"mock-socket": "~9.3.1",
"start-server-and-test": "~2.0.0"
},
"scarfSettings": {

View File

@@ -1,28 +1,11 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AppPreloadingStrategy } from './app.preloading-strategy'
import { StartComponent } from './components/start/start.component';
import { TransactionComponent } from './components/transaction/transaction.component';
import { BlockComponent } from './components/block/block.component';
import { BlockViewComponent } from './components/block-view/block-view.component';
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component';
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
import { ClockComponent } from './components/clock/clock.component';
import { AddressComponent } from './components/address/address.component';
import { MasterPageComponent } from './components/master-page/master-page.component';
import { AboutComponent } from './components/about/about.component';
import { StatusViewComponent } from './components/status-view/status-view.component';
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
import { TrademarkPolicyComponent } from './components/trademark-policy/trademark-policy.component';
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
import { BlocksList } from './components/blocks-list/blocks-list.component';
import { RbfList } from './components/rbf-list/rbf-list.component';
import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component';
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
import { AssetsComponent } from './components/assets/assets.component';
import { AssetComponent } from './components/asset/asset.component';
import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component';
import { CalculatorComponent } from './components/calculator/calculator.component';
const browserWindow = window || {};
// @ts-ignore
@@ -35,95 +18,13 @@ let routes: Routes = [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule),
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '',
component: MasterPageComponent,
children: [
{
path: 'mining/blocks',
redirectTo: 'blocks',
pathMatch: 'full'
},
{
path: 'tx/push',
component: PushTransactionComponent,
},
{
path: 'about',
component: AboutComponent,
},
{
path: 'blocks',
component: BlocksList,
},
{
path: 'rbf',
component: RbfList,
},
{
path: 'terms-of-service',
component: TermsOfServiceComponent
},
{
path: 'privacy-policy',
component: PrivacyPolicyComponent
},
{
path: 'trademark-policy',
component: TrademarkPolicyComponent
},
{
path: 'address/:id',
children: [],
component: AddressComponent,
data: {
ogImage: true,
networkSpecific: true,
}
},
{
path: 'tx',
component: StartComponent,
data: { networkSpecific: true },
children: [
{
path: ':id',
component: TransactionComponent
},
],
},
{
path: 'block',
component: StartComponent,
data: { networkSpecific: true },
children: [
{
path: ':id',
component: BlockComponent,
data: {
ogImage: true
}
},
],
},
{
path: 'docs',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule),
data: { preload: true },
},
{
path: 'api',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
},
{
path: 'lightning',
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule),
data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true, networks: ['bitcoin'] },
},
],
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'status',
@@ -132,7 +33,8 @@ let routes: Routes = [
},
{
path: '',
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '**',
@@ -151,88 +53,13 @@ let routes: Routes = [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '',
component: MasterPageComponent,
children: [
{
path: 'tx/push',
component: PushTransactionComponent,
},
{
path: 'about',
component: AboutComponent,
},
{
path: 'blocks',
component: BlocksList,
},
{
path: 'rbf',
component: RbfList,
},
{
path: 'terms-of-service',
component: TermsOfServiceComponent
},
{
path: 'privacy-policy',
component: PrivacyPolicyComponent
},
{
path: 'trademark-policy',
component: TrademarkPolicyComponent
},
{
path: 'address/:id',
children: [],
component: AddressComponent,
data: {
ogImage: true,
networkSpecific: true,
}
},
{
path: 'tx',
data: { networkSpecific: true },
component: StartComponent,
children: [
{
path: ':id',
component: TransactionComponent
},
],
},
{
path: 'block',
data: { networkSpecific: true },
component: StartComponent,
children: [
{
path: ':id',
component: BlockComponent,
data: {
ogImage: true
}
},
],
},
{
path: 'docs',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
},
{
path: 'api',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
},
{
path: 'lightning',
data: { networks: ['bitcoin'] },
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
},
],
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'status',
@@ -241,7 +68,8 @@ let routes: Routes = [
},
{
path: '',
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '**',
@@ -252,97 +80,13 @@ let routes: Routes = [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '',
component: MasterPageComponent,
children: [
{
path: 'mining/blocks',
redirectTo: 'blocks',
pathMatch: 'full'
},
{
path: 'tx/push',
component: PushTransactionComponent,
},
{
path: 'about',
component: AboutComponent,
},
{
path: 'blocks',
component: BlocksList,
},
{
path: 'rbf',
component: RbfList,
},
{
path: 'tools/calculator',
component: CalculatorComponent
},
{
path: 'terms-of-service',
component: TermsOfServiceComponent
},
{
path: 'privacy-policy',
component: PrivacyPolicyComponent
},
{
path: 'trademark-policy',
component: TrademarkPolicyComponent
},
{
path: 'address/:id',
children: [],
component: AddressComponent,
data: {
ogImage: true,
networkSpecific: true,
}
},
{
path: 'tx',
data: { networkSpecific: true },
component: StartComponent,
children: [
{
path: ':id',
component: TransactionComponent
},
],
},
{
path: 'block',
data: { networkSpecific: true },
component: StartComponent,
children: [
{
path: ':id',
component: BlockComponent,
data: {
ogImage: true
}
},
],
},
{
path: 'docs',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
},
{
path: 'api',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
},
{
path: 'lightning',
data: { networks: ['bitcoin'] },
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
},
],
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'preview',
@@ -373,6 +117,18 @@ let routes: Routes = [
path: 'clock/:mode/:index',
component: ClockComponent,
},
{
path: 'view/block/:id',
component: BlockViewComponent,
},
{
path: 'view/mempool-block/:index',
component: MempoolBlockViewComponent,
},
{
path: 'view/blocks',
component: EightBlocksComponent,
},
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },
@@ -380,7 +136,8 @@ let routes: Routes = [
},
{
path: '',
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '**',
@@ -391,7 +148,6 @@ let routes: Routes = [
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'bisq') {
routes = [{
path: '',
component: BisqMasterPageComponent,
loadChildren: () => import('./bisq/bisq.module').then(m => m.BisqModule)
}];
}
@@ -404,105 +160,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true },
},
{
path: '',
component: LiquidMasterPageComponent,
children: [
{
path: 'tx/push',
component: PushTransactionComponent,
},
{
path: 'about',
component: AboutComponent,
},
{
path: 'blocks',
component: BlocksList,
},
{
path: 'terms-of-service',
component: TermsOfServiceComponent
},
{
path: 'privacy-policy',
component: PrivacyPolicyComponent
},
{
path: 'trademark-policy',
component: TrademarkPolicyComponent
},
{
path: 'address/:id',
children: [],
component: AddressComponent,
data: {
ogImage: true,
networkSpecific: true,
}
},
{
path: 'tx',
data: { networkSpecific: true },
component: StartComponent,
children: [
{
path: ':id',
component: TransactionComponent
},
],
},
{
path: 'block',
data: { networkSpecific: true },
component: StartComponent,
children: [
{
path: ':id',
component: BlockComponent,
data: {
ogImage: true
}
},
],
},
{
path: 'assets',
data: { networks: ['liquid'] },
component: AssetsNavComponent,
children: [
{
path: 'all',
data: { networks: ['liquid'] },
component: AssetsComponent,
},
{
path: 'asset/:id',
data: { networkSpecific: true },
component: AssetComponent
},
{
path: 'group/:id',
data: { networkSpecific: true },
component: AssetGroupComponent
},
{
path: '**',
redirectTo: 'all'
}
]
},
{
path: 'docs',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
},
{
path: 'api',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
},
],
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
data: { preload: true },
},
{
path: 'status',
@@ -511,7 +175,8 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
},
{
path: '',
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true },
},
{
path: '**',
@@ -522,110 +187,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true },
},
{
path: '',
component: LiquidMasterPageComponent,
children: [
{
path: 'tx/push',
component: PushTransactionComponent,
},
{
path: 'about',
component: AboutComponent,
},
{
path: 'blocks',
component: BlocksList,
},
{
path: 'terms-of-service',
component: TermsOfServiceComponent
},
{
path: 'privacy-policy',
component: PrivacyPolicyComponent
},
{
path: 'trademark-policy',
component: TrademarkPolicyComponent
},
{
path: 'address/:id',
children: [],
component: AddressComponent,
data: {
ogImage: true,
networkSpecific: true,
}
},
{
path: 'tx',
data: { networkSpecific: true },
component: StartComponent,
children: [
{
path: ':id',
component: TransactionComponent
},
],
},
{
path: 'block',
data: { networkSpecific: true },
component: StartComponent,
children: [
{
path: ':id',
component: BlockComponent,
data: {
ogImage: true
}
},
],
},
{
path: 'assets',
data: { networks: ['liquid'] },
component: AssetsNavComponent,
children: [
{
path: 'featured',
data: { networkSpecific: true },
component: AssetsFeaturedComponent,
},
{
path: 'all',
data: { networks: ['liquid'] },
component: AssetsComponent,
},
{
path: 'asset/:id',
data: { networkSpecific: true },
component: AssetComponent
},
{
path: 'group/:id',
data: { networkSpecific: true },
component: AssetGroupComponent
},
{
path: '**',
redirectTo: 'featured'
}
]
},
{
path: 'docs',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
},
{
path: 'api',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
},
],
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
data: { preload: true },
},
{
path: 'preview',
@@ -647,7 +215,8 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
},
{
path: '',
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true },
},
{
path: '**',

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 Project™. See Bisq market prices, trading activity, and more.`);
this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Source Project™. See Bisq market prices, trading activity, and more.`);
this.websocketService.want(['blocks']);
this.volumes$ = this.bisqApiService.getAllVolumesDay$()

View File

@@ -27,9 +27,11 @@ import { AutofocusDirective } from '../components/ngx-bootstrap-multiselect/auto
import { MultiSelectSearchFilter } from '../components/ngx-bootstrap-multiselect/search-filter.pipe';
import { OffClickDirective } from '../components/ngx-bootstrap-multiselect/off-click.directive';
import { NgxDropdownMultiselectComponent } from '../components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component';
import { BisqMasterPageComponent } from '../components/bisq-master-page/bisq-master-page.component';
@NgModule({
declarations: [
BisqMasterPageComponent,
BisqTransactionsComponent,
BisqTransactionComponent,
BisqBlockComponent,

View File

@@ -1,6 +1,6 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AboutComponent } from '../components/about/about.component';
import { BisqMasterPageComponent } from '../components/bisq-master-page/bisq-master-page.component';
import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component';
import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component';
import { BisqBlockComponent } from './bisq-block/bisq-block.component';
@@ -10,78 +10,83 @@ import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
import { BisqDashboardComponent } from './bisq-dashboard/bisq-dashboard.component';
import { BisqMarketComponent } from './bisq-market/bisq-market.component';
import { BisqMainDashboardComponent } from './bisq-main-dashboard/bisq-main-dashboard.component';
import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component';
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
const routes: Routes = [
{
path: '',
component: BisqMainDashboardComponent,
},
{
path: 'markets',
data: { networks: ['bisq'] },
component: BisqDashboardComponent,
},
{
path: 'transactions',
data: { networks: ['bisq'] },
component: BisqTransactionsComponent
},
{
path: 'market/:pair',
data: { networkSpecific: true },
component: BisqMarketComponent,
},
{
path: 'tx/push',
component: PushTransactionComponent,
},
{
path: 'tx/:id',
data: { networkSpecific: true },
component: BisqTransactionComponent
},
{
path: 'blocks',
children: [],
component: BisqBlocksComponent
},
{
path: 'block/:id',
data: { networkSpecific: true },
component: BisqBlockComponent,
},
{
path: 'address/:id',
data: { networkSpecific: true },
component: BisqAddressComponent,
},
{
path: 'stats',
data: { networks: ['bisq'] },
component: BisqStatsComponent,
},
{
path: 'about',
component: AboutComponent,
},
{
path: 'docs',
loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule)
},
{
path: 'api',
loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule)
},
{
path: 'terms-of-service',
component: TermsOfServiceComponent
},
{
path: '**',
redirectTo: ''
}
{
path: '',
component: BisqMasterPageComponent,
children: [
{
path: '',
component: BisqMainDashboardComponent,
},
{
path: 'markets',
data: { networks: ['bisq'] },
component: BisqDashboardComponent,
},
{
path: 'transactions',
data: { networks: ['bisq'] },
component: BisqTransactionsComponent
},
{
path: 'market/:pair',
data: { networkSpecific: true },
component: BisqMarketComponent,
},
{
path: 'tx/push',
component: PushTransactionComponent,
},
{
path: 'tx/:id',
data: { networkSpecific: true },
component: BisqTransactionComponent
},
{
path: 'blocks',
children: [],
component: BisqBlocksComponent
},
{
path: 'block/:id',
data: { networkSpecific: true },
component: BisqBlockComponent,
},
{
path: 'address/:id',
data: { networkSpecific: true },
component: BisqAddressComponent,
},
{
path: 'stats',
data: { networks: ['bisq'] },
component: BisqStatsComponent,
},
{
path: 'about',
loadChildren: () => import('../components/about/about.module').then(m => m.AboutModule),
},
{
path: 'docs',
loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule)
},
{
path: 'api',
loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule)
},
{
path: 'terms-of-service',
loadChildren: () => import('../components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule),
},
{
path: '**',
redirectTo: ''
}
]
}
];
@NgModule({

View File

@@ -0,0 +1,37 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { MasterPageComponent } from './components/master-page/master-page.component';
const routes: Routes = [
{
path: '',
component: MasterPageComponent,
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule),
data: { preload: true },
}
];
@NgModule({
imports: [
RouterModule.forChild(routes)
],
exports: [
RouterModule
]
})
export class BitcoinGraphsRoutingModule { }
@NgModule({
imports: [
CommonModule,
BitcoinGraphsRoutingModule,
],
})
export class BitcoinGraphsModule { }

View File

@@ -32,11 +32,22 @@
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
</video>
<ng-container *ngIf="false && officialMempoolSpace">
<h3 class="mt-5">Sponsor the project</h3>
<div class="d-flex justify-content-center" style="max-width: 90%; margin: 35px auto 75px auto; column-gap: 15px">
<a href="/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">Community</a>
<a href="/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">Enterprise</a>
<ng-container *ngIf="officialMempoolSpace">
<div id="become-sponsor-container">
<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">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">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>
</ng-container>
@@ -193,7 +204,7 @@
<ng-container>
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</ng-container>
@@ -205,7 +216,7 @@
<div class="wrapper">
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</div>
@@ -299,9 +310,9 @@
<img class="image" src="/resources/profile/blixt.png" />
<span>Blixt</span>
</a>
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="Zeus">
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="ZEUS">
<img class="image" src="/resources/profile/zeus.png" />
<span>Zeus</span>
<span>ZEUS</span>
</a>
<a href="https://github.com/vulpemventures/marina" target="_blank" title="Marina Wallet">
<img class="image" src="/resources/profile/marina.svg" />

View File

@@ -246,3 +246,49 @@
width: 64px;
height: 64px;
}
#become-sponsor-container {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
gap: 20px;
margin: 68px auto;
}
.become-sponsor {
background-color: #1d1f31;
border-radius: 16px;
padding: 12px 20px;
width: 400px;
padding: 40px 20px;
}
.become-sponsor a {
margin-top: 10px;
}
#become-sponsor-container .btn {
margin-bottom: 24px;
}
#become-sponsor-container .ng-fa-icon {
color: #2ecc71;
margin-right: 5px;
}
#become-sponsor-container .sponsor-feature {
text-align: left;
width: 250px;
margin: 12px auto;
white-space: nowrap;
}
@media (max-width: 992px) {
#become-sponsor-container {
flex-wrap: wrap;
}
}

View File

@@ -0,0 +1,40 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { AboutComponent } from './about.component';
import { SharedModule } from '../../shared/shared.module';
const routes: Routes = [
{
path: '',
component: AboutComponent,
}
];
@NgModule({
imports: [
RouterModule.forChild(routes)
],
exports: [
RouterModule
]
})
export class AboutRoutingModule { }
@NgModule({
imports: [
CommonModule,
AboutRoutingModule,
SharedModule,
],
declarations: [
AboutComponent,
]
})
export class AboutModule { }

View File

@@ -2,7 +2,6 @@
height: 100%;
min-width: 120px;
width: 120px;
max-height: 90vh;
margin-left: 4em;
margin-right: 1.5em;
padding-bottom: 63px;
@@ -18,6 +17,7 @@
bottom: 0;
left: 0;
right: 0;
min-height: 30px;
display: flex;
flex-direction: column;
justify-content: center;

View File

@@ -58,13 +58,15 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
fee: option.fee,
}
});
bars.push({
rate: this.estimate.targetFeeRate,
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
class: 'target',
label: 'next block',
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
});
if (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee) {
bars.push({
rate: this.estimate.targetFeeRate,
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
class: 'target',
label: 'next block',
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
});
}
bars.push({
rate: baseRate,
style: this.getStyle(baseRate, maxRate, 0),

View File

@@ -1,14 +1,16 @@
<span id="successAlert" class="m-0 p-0 d-block" style="height: 1px;"></span>
<div class="row" *ngIf="showSuccess">
<div class="col" id="successAlert">
<div class="col">
<div class="alert alert-success">
Transaction has now been submitted to mining pools for acceleration. You can track the progress <a class="alert-link" routerLink="/services/accelerator/history">here</a>.
Transaction has now been <a class="alert-link" routerLink="/services/accelerator/history">submitted</a> to mining pools for acceleration.
</div>
</div>
</div>
<span id="mempoolError" class="m-0 p-0 d-block" style="height: 1px;"></span>
<div class="row" *ngIf="error">
<div class="col" id="mempoolError">
<app-mempool-error [error]="error"></app-mempool-error>
<div class="col">
<app-mempool-error [error]="error" [alertClass]="error === 'waitlisted' ? 'alert-mempool' : 'alert-danger'"></app-mempool-error>
</div>
</div>
@@ -37,10 +39,10 @@
<td class="item">
Virtual size
</td>
<td class="units" [innerHTML]="'&lrm;' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
<td style="text-align: end;" [innerHTML]="'&lrm;' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
</tr>
<tr class="info">
<td class="info">
<td class="info" colspan=3>
<i><small>Size in vbytes of this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
</td>
</tr>
@@ -48,12 +50,12 @@
<td class="item">
In-band fees
</td>
<td class="units">
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats|sats">sats</span>
<td style="text-align: end;">
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats">sats</span>
</td>
</tr>
<tr class="info group-last">
<td class="info">
<td class="info" colspan=3>
<i><small>Fees already paid by this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
</td>
</tr>
@@ -74,8 +76,8 @@
<div class="d-flex mb-0">
<ng-container *ngFor="let option of maxRateOptions">
<button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === option.index}" (click)="setUserBid(option)">
<span class="fee">{{ option.fee | number }} <span class="symbol" i18n="shared.sats|sats">sats</span></span>
<span class="rate">~ <app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span>
<span class="fee">{{ option.fee + estimate.mempoolBaseFee + estimate.vsizeFee | number }} <span class="symbol" i18n="shared.sats">sats</span></span>
<span class="rate">~<app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span>
</button>
</ng-container>
</div>
@@ -87,23 +89,15 @@
<h5>Acceleration summary</h5>
<div class="row mb-3">
<div class="col">
<div class="table-toggle btn-group btn-group-toggle">
<div class="btn btn-primary btn-sm" [class.active]="showTable === 'estimated'" (click)="showTable = 'estimated'">
<span>Estimated cost</span>
</div>
<div class="btn btn-primary btn-sm" [class.active]="showTable === 'maximum'" (click)="showTable = 'maximum'">
<span>Maximum cost</span>
</div>
</div>
<table class="table table-borderless table-border table-dark table-accelerator">
<tbody>
<!-- ESTIMATED FEE -->
<ng-container *ngIf="showTable === 'estimated'">
<ng-container>
<tr class="group-first">
<td class="item">
Next block market rate
</td>
<td class="amt" style="font-size: 20px">
<td class="amt" style="font-size: 16px">
{{ estimate.targetFeeRate | number : '1.0-0' }}
</td>
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
@@ -116,34 +110,8 @@
{{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span>
</td>
</tr>
</ng-container>
<!-- USER MAX BID -->
<ng-container *ngIf="showTable === 'maximum'">
<tr class="group-first">
<td class="item">
Your maximum
</td>
<td class="amt" style="width: 45%; font-size: 20px">
~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }}
</td>
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
</tr>
<tr class="info">
<td class="info">
<i><small>The maximum extra transaction fee you could pay</small></i>
</td>
<td class="amt">
<span>
{{ userBid | number }}
</span>
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat"><app-fiat [value]="userBid"></app-fiat></span>
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span>
</td>
</tr>
</ng-container>
@@ -162,11 +130,11 @@
+{{ estimate.mempoolBaseFee | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
</td>
</tr>
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
<tr class="info group-last">
<td class="info">
<i><small>Transaction vsize fee</small></i>
</td>
@@ -174,14 +142,14 @@
+{{ estimate.vsizeFee | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span>
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span>
</td>
</tr>
<!-- NEXT BLOCK ESTIMATE -->
<ng-container *ngIf="showTable === 'estimated'">
<tr class="group-first">
<ng-container>
<tr class="group-first" style="border-top: 1px dashed grey; border-collapse: collapse;">
<td class="item">
<b style="background-color: #5E35B1" class="p-1 pl-0">Estimated acceleration cost</b>
</td>
@@ -191,19 +159,19 @@
</span>
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span>
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span>
</td>
</tr>
<tr class="info group-last">
<td class="info">
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
<td class="info" colspan=3>
<i><small>If your tx is accelerated to </small><small>{{ estimate.targetFeeRate | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
</td>
</tr>
</ng-container>
<!-- MAX COST -->
<ng-container *ngIf="showTable === 'maximum'">
<ng-container>
<tr class="group-first">
<td class="item">
<b style="background-color: #105fb0;" class="p-1 pl-0">Maximum acceleration cost</b>
@@ -214,21 +182,21 @@
</span>
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1">
<app-fiat [value]="maxCost" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
</span>
</td>
</tr>
<tr class="info group-last">
<td class="info">
<td class="info" colspan=3>
<i><small>If your tx is accelerated to </small><small>~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
</td>
</tr>
</ng-container>
<!-- USER BALANCE -->
<ng-container *ngIf="estimate.userBalance < maxCost">
<ng-container *ngIf="isLoggedIn() && estimate.userBalance < maxCost">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item">
Available balance
@@ -237,13 +205,24 @@
{{ estimate.userBalance | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1">
<app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
</span>
</td>
</tr>
</ng-container>
<!-- LOGIN CTA -->
<ng-container *ngIf="!isLoggedIn()">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item"></td>
<td class="amt"></td>
<td class="units d-flex">
<a [routerLink]="['/login']" [queryParams]="{redirectTo: '/tx/' + tx.txid + '#accelerate'}" class="btn btn-purple flex-grow-1">Login</a>
</td>
</tr>
</ng-container>
</tbody>
</table>
</div>

View File

@@ -8,9 +8,6 @@
align-items: center;
justify-content: center;
.fee {
font-size: 1.2em;
}
.rate {
font-size: 0.9em;
.symbol {
@@ -28,7 +25,10 @@
.feerate.active {
background-color: #105fb0 !important;
opacity: 1;
border: 1px solid white !important;
border: 1px solid #007fff !important;
}
.feerate:focus {
box-shadow: none !important;
}
.estimateDisabled {
@@ -41,10 +41,26 @@
margin-top: 0.5em;
}
.tab {
&:first-child {
margin-right: 1px;
}
border: solid 1px black;
border-bottom: none;
background-color: #323655;
border-top-left-radius: 10px !important;
border-top-right-radius: 10px !important;
}
.tab.active {
background-color: #5d659d !important;
opacity: 1;
}
.tab:focus {
box-shadow: none !important;
}
.table-accelerator {
tr {
text-wrap: wrap;
td {
padding-top: 0;
padding-bottom: 0;
@@ -68,6 +84,7 @@
}
&.info {
color: #6c757d;
white-space: initial;
}
&.amt {
text-align: right;
@@ -76,6 +93,9 @@
&.units {
padding-left: 0.2em;
white-space: nowrap;
display: flex;
justify-content: space-between;
align-items: center;
}
}
}
@@ -85,4 +105,8 @@
flex-direction: row;
align-items: stretch;
margin-top: 1em;
}
.item {
white-space: initial;
}

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener } from '@angular/core';
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener, ChangeDetectorRef } from '@angular/core';
import { ApiService } from '../../services/api.service';
import { Subscription, catchError, of, tap } from 'rxjs';
import { StorageService } from '../../services/storage.service';
@@ -55,14 +55,14 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
maxCost = 0;
userBid = 0;
selectFeeRateIndex = 1;
showTable: 'estimated' | 'maximum' = 'maximum';
isMobile: boolean = window.innerWidth <= 767.98;
maxRateOptions: RateOption[] = [];
constructor(
private apiService: ApiService,
private storageService: StorageService
private storageService: StorageService,
private cd: ChangeDetectorRef
) { }
ngOnDestroy(): void {
@@ -73,7 +73,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
ngOnChanges(changes: SimpleChanges): void {
if (changes.scrollEvent) {
this.scrollToPreview('acceleratePreviewAnchor', 'center');
this.scrollToPreview('acceleratePreviewAnchor', 'start');
}
}
@@ -126,7 +126,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
if (!this.error) {
this.scrollToPreview('acceleratePreviewAnchor', 'center');
this.scrollToPreview('acceleratePreviewAnchor', 'start');
}
}
}),
@@ -162,13 +162,14 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
scrollToPreview(id: string, position: ScrollLogicalPosition) {
const acceleratePreviewAnchor = document.getElementById(id);
if (acceleratePreviewAnchor) {
this.cd.markForCheck();
acceleratePreviewAnchor.scrollIntoView({
behavior: 'smooth',
inline: position,
block: position,
});
}
}
}
/**
* Send acceleration request
@@ -187,7 +188,11 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
this.estimateSubscription.unsubscribe();
},
error: (response) => {
this.error = response.error;
if (response.status === 403 && response.error === 'not_available') {
this.error = 'waitlisted';
} else {
this.error = response.error;
}
this.scrollToPreviewWithTimeout('mempoolError', 'center');
}
});

View File

@@ -1,4 +1,4 @@
<header>
<header class="sticky-header">
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">

View File

@@ -1,3 +1,11 @@
.sticky-header {
position: sticky;
position: -webkit-sticky;
top: 0;
width: 100%;
z-index: 100;
}
li.nav-item.active {
background-color: #653b9c;
}

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
import { EChartsOption } from 'echarts';
import { EChartsOption } from '../../graphs/echarts';
import { Observable, Subscription, combineLatest } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { EChartsOption, graphic } from 'echarts';
import { echarts, EChartsOption } from '../../graphs/echarts';
import { Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
@@ -123,11 +123,11 @@ export class BlockFeesGraphComponent implements OnInit {
this.chartOptions = {
title: title,
color: [
new graphic.LinearGradient(0, 0, 0, 1, [
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#FDD835' },
{ offset: 1, color: '#FB8C00' },
]),
new graphic.LinearGradient(0, 0, 0, 1, [
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#C0CA33' },
{ offset: 1, color: '#1B5E20' },
]),

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
import { EChartsOption } from 'echarts';
import { EChartsOption } from '../../graphs/echarts';
import { Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';

View File

@@ -20,6 +20,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() blockLimit: number;
@Input() orientation = 'left';
@Input() flip = true;
@Input() animationDuration: number = 1000;
@Input() animationOffset: number | null = null;
@Input() disableSpinner = false;
@Input() mirrorTxid: string | void;
@Input() unavailable: boolean = false;
@@ -141,9 +143,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
}
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true): void {
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void {
if (this.scene) {
this.scene.replace(transactions || [], direction, sort);
this.scene.replace(transactions || [], direction, sort, startTime);
this.start();
this.updateSearchHighlight();
}
@@ -226,7 +228,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} else {
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
highlighting: this.auditHighlighting });
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset });
this.start();
}
}

View File

@@ -9,6 +9,9 @@ export default class BlockScene {
txs: { [key: string]: TxView };
orientation: string;
flip: boolean;
animationDuration: number = 1000;
configAnimationOffset: number | null;
animationOffset: number;
highlightingEnabled: boolean;
width: number;
height: number;
@@ -23,11 +26,11 @@ export default class BlockScene {
animateUntil = 0;
dirty: boolean;
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number,
constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
) {
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting });
this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting });
}
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
@@ -36,6 +39,7 @@ export default class BlockScene {
this.gridSize = this.width / this.gridWidth;
this.unitPadding = Math.max(1, Math.floor(this.gridSize / 5));
this.unitWidth = this.gridSize - (this.unitPadding * 2);
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
this.dirty = true;
if (this.initialised && this.scene) {
@@ -90,8 +94,8 @@ export default class BlockScene {
}
// Animate new block entering scene
enter(txs: TransactionStripped[], direction) {
this.replace(txs, direction);
enter(txs: TransactionStripped[], direction, startTime?: number) {
this.replace(txs, direction, false, startTime);
}
// Animate block leaving scene
@@ -108,8 +112,7 @@ export default class BlockScene {
}
// Reset layout and replace with new set of transactions
replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true): void {
const startTime = performance.now();
replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true, startTime: number = performance.now()): void {
const nextIds = {};
const remove = [];
txs.forEach(tx => {
@@ -133,7 +136,7 @@ export default class BlockScene {
removed.forEach(tx => {
tx.destroy();
});
}, 1000);
}, (startTime - performance.now()) + this.animationDuration + 1000);
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
@@ -147,7 +150,7 @@ export default class BlockScene {
});
}
this.updateAll(startTime, 200, direction);
this.updateAll(startTime, 50, direction);
}
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
@@ -214,10 +217,13 @@ export default class BlockScene {
this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
}
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number,
private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
): void {
this.animationDuration = animationDuration || 1000;
this.configAnimationOffset = animationOffset;
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
this.orientation = orientation;
this.flip = flip;
this.vertexArray = vertexArray;
@@ -261,8 +267,8 @@ export default class BlockScene {
this.applyTxUpdate(tx, {
display: {
position: {
x: tx.screenPosition.x + (direction === 'right' ? -this.width : (direction === 'left' ? this.width : 0)) * 1.4,
y: tx.screenPosition.y + (direction === 'up' ? -this.height : (direction === 'down' ? this.height : 0)) * 1.4,
x: tx.screenPosition.x + (direction === 'right' ? -this.width - this.animationOffset : (direction === 'left' ? this.width + this.animationOffset : 0)),
y: tx.screenPosition.y + (direction === 'up' ? -this.height - this.animationOffset : (direction === 'down' ? this.height + this.animationOffset : 0)),
s: tx.screenPosition.s
},
color: txColor,
@@ -275,7 +281,7 @@ export default class BlockScene {
position: tx.screenPosition,
color: txColor
},
duration: animate ? 1000 : 1,
duration: animate ? this.animationDuration : 1,
start: startTime,
delay: animate ? delay : 0,
});
@@ -284,8 +290,8 @@ export default class BlockScene {
display: {
position: tx.screenPosition
},
duration: animate ? 1000 : 0,
minDuration: animate ? 500 : 0,
duration: animate ? this.animationDuration : 0,
minDuration: animate ? (this.animationDuration / 2) : 0,
start: startTime,
delay: animate ? delay : 0,
adjust: animate
@@ -322,11 +328,11 @@ export default class BlockScene {
this.applyTxUpdate(tx, {
display: {
position: {
x: tx.screenPosition.x + (direction === 'right' ? this.width : (direction === 'left' ? -this.width : 0)) * 1.4,
y: tx.screenPosition.y + (direction === 'up' ? this.height : (direction === 'down' ? -this.height : 0)) * 1.4,
x: tx.screenPosition.x + (direction === 'right' ? this.width + this.animationOffset : (direction === 'left' ? -this.width - this.animationOffset : 0)),
y: tx.screenPosition.y + (direction === 'up' ? this.height + this.animationOffset : (direction === 'down' ? -this.height - this.animationOffset : 0)),
}
},
duration: 1000,
duration: this.animationDuration,
start: startTime,
delay: 50
});

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { EChartsOption, graphic } from 'echarts';
import { echarts, EChartsOption } from '../../graphs/echarts';
import { Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
@@ -123,11 +123,11 @@ export class BlockRewardsGraphComponent implements OnInit {
title: title,
animation: false,
color: [
new graphic.LinearGradient(0, 0, 0, 1, [
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#FDD835' },
{ offset: 1, color: '#FB8C00' },
]),
new graphic.LinearGradient(0, 0, 0, 1, [
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#C0CA33' },
{ offset: 1, color: '#1B5E20' },
]),

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
import { EChartsOption} from 'echarts';
import { EChartsOption} from '../../graphs/echarts';
import { Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';

View File

@@ -0,0 +1,14 @@
<div class="block-wrapper">
<div class="block-container">
<app-block-overview-graph
#blockGraph
[isLoading]="false"
[resolution]="resolution"
[blockLimit]="stateService.blockVSize"
[orientation]="'top'"
[flip]="false"
[disableSpinner]="true"
(txClickEvent)="onTxClick($event)"
></app-block-overview-graph>
</div>
</div>

View File

@@ -0,0 +1,22 @@
.block-wrapper {
width: 100vw;
height: 100vh;
background: #181b2d;
}
.block-container {
flex-grow: 0;
flex-shrink: 0;
width: 100vw;
max-width: 100vh;
height: 100vh;
padding: 0;
margin: auto;
display: flex;
justify-content: center;
align-items: center;
* {
flex-grow: 1;
}
}

View File

@@ -0,0 +1,180 @@
import { Component, OnInit, OnDestroy, ViewChild, HostListener } from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { switchMap, tap, catchError, shareReplay, filter } from 'rxjs/operators';
import { of, Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
function bestFitResolution(min, max, n): number {
const target = (min + max) / 2;
let bestScore = Infinity;
let best = null;
for (let i = min; i <= max; i++) {
const remainder = (n % i);
if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) {
bestScore = remainder;
best = i;
}
}
return best;
}
@Component({
selector: 'app-block-view',
templateUrl: './block-view.component.html',
styleUrls: ['./block-view.component.scss']
})
export class BlockViewComponent implements OnInit, OnDestroy {
network = '';
block: BlockExtended;
blockHeight: number;
blockHash: string;
rawId: string;
isLoadingBlock = true;
strippedTransactions: TransactionStripped[];
isLoadingOverview = true;
autofit: boolean = false;
resolution: number = 80;
overviewSubscription: Subscription;
networkChangedSubscription: Subscription;
queryParamsSubscription: Subscription;
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
constructor(
private route: ActivatedRoute,
private router: Router,
private electrsApiService: ElectrsApiService,
public stateService: StateService,
private seoService: SeoService,
private apiService: ApiService
) { }
ngOnInit(): void {
this.network = this.stateService.network;
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
this.autofit = params.autofit === 'true';
if (this.autofit) {
this.onResize();
}
});
const block$ = this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
this.rawId = params.get('id') || '';
const blockHash: string = params.get('id') || '';
this.block = undefined;
let isBlockHeight = false;
if (/^[0-9]+$/.test(blockHash)) {
isBlockHeight = true;
} else {
this.blockHash = blockHash;
}
this.isLoadingBlock = true;
this.isLoadingOverview = true;
if (isBlockHeight) {
return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockHash, 10))
.pipe(
switchMap((hash) => {
if (hash) {
this.blockHash = hash;
return this.apiService.getBlock$(hash);
} else {
return null;
}
}),
catchError(() => {
return of(null);
}),
);
}
return this.apiService.getBlock$(blockHash);
}),
filter((block: BlockExtended | void) => block != null),
tap((block: BlockExtended) => {
this.block = block;
this.blockHeight = block.height;
this.seoService.setTitle($localize`:@@block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.id}:BLOCK_ID:`);
if( this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ) {
this.seoService.setDescription($localize`:@@meta.description.liquid.block:See size, weight, fee range, included transactions, and more for Liquid${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`);
} else {
this.seoService.setDescription($localize`:@@meta.description.bitcoin.block:See size, weight, fee range, included transactions, audit (expected v actual), and more for Bitcoin${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`);
}
this.isLoadingBlock = false;
this.isLoadingOverview = true;
}),
shareReplay(1)
);
this.overviewSubscription = block$.pipe(
switchMap((block) => this.apiService.getStrippedBlockTransactions$(block.id)
.pipe(
catchError(() => {
return of([]);
}),
switchMap((transactions) => {
return of(transactions);
})
)
),
)
.subscribe((transactions: TransactionStripped[]) => {
this.strippedTransactions = transactions;
this.isLoadingOverview = false;
if (this.blockGraph) {
this.blockGraph.destroy();
this.blockGraph.setup(this.strippedTransactions);
}
},
() => {
this.isLoadingOverview = false;
if (this.blockGraph) {
this.blockGraph.destroy();
}
});
this.networkChangedSubscription = this.stateService.networkChanged$
.subscribe((network) => this.network = network);
}
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
if (!event.keyModifier) {
this.router.navigate([url]);
} else {
window.open(url, '_blank');
}
}
@HostListener('window:resize', ['$event'])
onResize(): void {
if (this.autofit) {
this.resolution = bestFitResolution(64, 96, Math.min(window.innerWidth, window.innerHeight));
}
}
ngOnDestroy(): void {
if (this.overviewSubscription) {
this.overviewSubscription.unsubscribe();
}
if (this.networkChangedSubscription) {
this.networkChangedSubscription.unsubscribe();
}
if (this.queryParamsSubscription) {
this.queryParamsSubscription.unsubscribe();
}
}
}

View File

@@ -262,7 +262,7 @@
<tbody>
<tr>
<td class="td-width" i18n="transaction.version">Version</td>
<td>{{ block.version | decimal2hex }} <span *ngIf="displayTaprootStatus() && hasTaproot(block.version)" class="badge badge-success ml-1" >Taproot</span></td>
<td>{{ block.version | decimal2hex }} <span *ngIf="displayTaprootStatus() && hasTaproot(block.version)" class="badge badge-success ml-1" i18n="tx-features.tag.taproot|Taproot">Taproot</span></td>
</tr>
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td i18n="block.bits">Bits</td>

View File

@@ -0,0 +1,43 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { BlockComponent } from './block.component';
import { SharedModule } from '../../shared/shared.module';
const routes: Routes = [
{
path: ':id',
component: BlockComponent,
data: {
ogImage: true
}
}
];
@NgModule({
imports: [
RouterModule.forChild(routes)
],
exports: [
RouterModule
]
})
export class BlockRoutingModule { }
@NgModule({
imports: [
CommonModule,
BlockRoutingModule,
SharedModule,
],
declarations: [
BlockComponent,
]
})
export class BlockModule { }

View File

@@ -55,7 +55,9 @@ export class BlocksList implements OnInit {
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
this.seoService.setTitle($localize`:@@meta.title.blocks-list:Blocks`);
if (!this.widget) {
this.seoService.setTitle($localize`:@@m8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
}
if( this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet' ) {
this.seoService.setDescription($localize`:@@meta.description.liquid.blocks:See the most recent Liquid${seoDescriptionNetwork(this.stateService.network)} blocks along with basic stats such as block height, block size, and more.`);
} else {

View File

@@ -1,6 +1,6 @@
<div class="container-xl">
<div class="text-center">
<h2>Calculator</h2>
<h2 i18n="shared.calculator">Calculator</h2>
</div>
<ng-container *ngIf="price$ | async; else loading">
@@ -26,7 +26,7 @@
<div class="input-group input-group-lg mb-1">
<div class="input-group-prepend">
<span class="input-group-text">sats</span>
<span class="input-group-text" i18n="shared.sats">sats</span>
</div>
<input type="text" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)">
<app-clipboard [button]="true" [text]="form.get('satoshis').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
@@ -41,7 +41,7 @@
<div class="bitcoin-satoshis-text">
<span [innerHTML]="form.get('bitcoin').value | bitcoinsatoshis"></span>
<span class="sats"> sats</span>
<span class="sats" i18n="shared.sats">sats</span>
</div>
</div>

View File

@@ -38,36 +38,35 @@
</div>
<ng-container *ngIf="!hideStats">
<div class="stats top left">
<p class="label" i18n="clock.fiat-price">fiat price</p>
<p class="label" i18n>Price</p>
<p>
<app-fiat [value]="100000000" digitsInfo="1.2-2" colorClass="white-color"></app-fiat>
</p>
</div>
<div class="stats top right">
<p class="label" i18n="clock.priority-rate|priority fee rate">priority rate</p>
<p class="label" i18n="fees-box.high-priority">High Priority</p>
<p *ngIf="recommendedFees$ | async as recommendedFees;">
<app-fee-rate [fee]="recommendedFees.fastestFee" unitClass="" rounding="1.0-0"></app-fee-rate>
</p>
</div>
<div *ngIf="mode !== 'mempool' && blocks?.length" class="stats bottom left">
<p [innerHTML]="blocks[blockIndex].size | bytes: 2"></p>
<p class="label" i18n="clock.block-size">block size</p>
<p class="label" i18n="block.size">Size</p>
</div>
<div *ngIf="mode !== 'mempool' && blocks?.length" class="stats bottom right">
<p class="force-wrap">
<ng-container *ngTemplateOutlet="blocks[blockIndex].tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: blocks[blockIndex].tx_count | number}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} <span class="label">transaction</span></ng-template>
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} <span class="label">transactions</span></ng-template>
{{ blocks[blockIndex].tx_count | number }}
<span class="label" i18n="dashboard.txs">Transactions</span>
</p>
</div>
<ng-container *ngIf="mempoolInfo$ | async as mempoolInfo;">
<div *ngIf="mode === 'mempool'" class="stats bottom left">
<p [innerHTML]="mempoolInfo.usage | bytes: 0"></p>
<p class="label" i18n="dashboard.memory-usage|Memory usage">memory usage</p>
<p class="label" i18n="dashboard.memory-usage|Memory usage">Memory Usage</p>
</div>
<div *ngIf="mode === 'mempool'" class="stats bottom right">
<p>{{ mempoolInfo.size | number }}</p>
<p class="label" i18n="dashboard.unconfirmed|Unconfirmed count">unconfirmed</p>
<p class="label" i18n="dashboard.unconfirmed|Unconfirmed count">Unconfirmed</p>
</div>
</ng-container>
</ng-container>

View File

@@ -63,6 +63,7 @@
.label {
font-size: calc(0.04 * var(--clock-width));
line-height: calc(0.05 * var(--clock-width));
text-transform: lowercase;
}
&.top {

View File

@@ -0,0 +1,24 @@
<div class="blocks" [class.wrap]="wrapBlocks">
<ng-container *ngFor="let i of blockIndices">
<div class="block-wrapper" [style]="wrapperStyle">
<div class="block-container" [style]="containerStyle">
<app-block-overview-graph
#blockGraph
[isLoading]="false"
[resolution]="resolution"
[blockLimit]="stateService.blockVSize"
[orientation]="'top'"
[flip]="false"
[animationDuration]="animationDuration"
[animationOffset]="animationOffset"
[disableSpinner]="true"
(txClickEvent)="onTxClick($event)"
></app-block-overview-graph>
<div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange>
<h1 class="height">{{ blockInfo[i].height }}</h1>
<h2 class="mined-by">by {{ blockInfo[i].extras.pool.name || 'Unknown' }}</h2>
</div>
</div>
</div>
</ng-container>
</div>

View File

@@ -0,0 +1,69 @@
.blocks {
width: 100%;
height: 100%;
min-width: 100vw;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: flex-start;
align-content: flex-start;
&.wrap {
flex-wrap: wrap;
}
.block-wrapper {
flex-grow: 0;
flex-shrink: 0;
position: relative;
--block-width: 1080px;
.info {
position: absolute;
left: 8%;
top: 8%;
right: 8%;
bottom: 8%;
height: 84%;
width: 84%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: calc(var(--block-width) * 0.03);
text-shadow: 0 0 calc(var(--block-width) * 0.05) black;
h1 {
font-size: 6em;
line-height: 1;
margin-bottom: calc(var(--block-width) * 0.03);
}
h2 {
font-size: 1.8em;
line-height: 1;
margin-bottom: calc(var(--block-width) * 0.03);
}
.hash {
font-family: monospace;
word-wrap: break-word;
font-size: 1.4em;
line-height: 1;
margin-bottom: calc(var(--block-width) * 0.03);
}
.mined-by {
position: absolute;
bottom: 0;
margin: auto;
text-align: center;
}
}
}
.block-container {
overflow: hidden;
}
}

View File

@@ -0,0 +1,253 @@
import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { catchError, startWith } from 'rxjs/operators';
import { Subject, Subscription, of } from 'rxjs';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
import { detectWebGL } from '../../shared/graphs.utils';
import { animate, style, transition, trigger } from '@angular/animations';
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
function bestFitResolution(min, max, n): number {
const target = (min + max) / 2;
let bestScore = Infinity;
let best = null;
for (let i = min; i <= max; i++) {
const remainder = (n % i);
if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) {
bestScore = remainder;
best = i;
}
}
return best;
}
interface BlockInfo extends BlockExtended {
timeString: string;
}
@Component({
selector: 'app-eight-blocks',
templateUrl: './eight-blocks.component.html',
styleUrls: ['./eight-blocks.component.scss'],
animations: [
trigger('infoChange', [
transition(':enter', [
style({ opacity: 0 }),
animate('1000ms', style({ opacity: 1 })),
]),
transition(':leave', [
animate('1000ms 500ms', style({ opacity: 0 }))
])
]),
],
})
export class EightBlocksComponent implements OnInit, OnDestroy {
network = '';
latestBlocks: BlockExtended[] = [];
isLoadingTransactions = true;
strippedTransactions: { [height: number]: TransactionStripped[] } = {};
webGlEnabled = true;
hoverTx: string | null = null;
blocksSubscription: Subscription;
cacheBlocksSubscription: Subscription;
networkChangedSubscription: Subscription;
queryParamsSubscription: Subscription;
graphChangeSubscription: Subscription;
numBlocks: number = 8;
blockIndices: number[] = [...Array(8).keys()];
autofit: boolean = false;
padding: number = 0;
wrapBlocks: boolean = false;
blockWidth: number = 1080;
animationDuration: number = 2000;
animationOffset: number = 0;
stagger: number = 0;
testing: boolean = true;
testHeight: number = 800000;
testShiftTimeout: number;
showInfo: boolean = true;
blockInfo: BlockInfo[] = [];
wrapperStyle = {
'--block-width': '1080px',
width: '1080px',
maxWidth: '1080px',
padding: '',
};
containerStyle = {};
resolution: number = 86;
@ViewChildren('blockGraph') blockGraphs: QueryList<BlockOverviewGraphComponent>;
constructor(
private route: ActivatedRoute,
private router: Router,
public stateService: StateService,
private websocketService: WebsocketService,
private apiService: ApiService,
private bytesPipe: BytesPipe,
) {
this.webGlEnabled = detectWebGL();
}
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.network = this.stateService.network;
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
this.numBlocks = Number.isInteger(Number(params.numBlocks)) ? Number(params.numBlocks) : 8;
this.blockIndices = [...Array(this.numBlocks).keys()];
this.autofit = params.autofit !== 'false';
this.padding = Number.isInteger(Number(params.padding)) ? Number(params.padding) : 10;
this.blockWidth = Number.isInteger(Number(params.blockWidth)) ? Number(params.blockWidth) : 540;
this.wrapBlocks = params.wrap !== 'false';
this.stagger = Number.isInteger(Number(params.stagger)) ? Number(params.stagger) : 0;
this.animationDuration = Number.isInteger(Number(params.animationDuration)) ? Number(params.animationDuration) : 2000;
this.animationOffset = this.padding * 2;
if (this.autofit) {
this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
} else {
this.resolution = 86;
}
this.wrapperStyle = {
'--block-width': this.blockWidth + 'px',
width: this.blockWidth + 'px',
maxWidth: this.blockWidth + 'px',
padding: (this.padding || 0) +'px 0px',
};
if (params.test === 'true') {
if (this.blocksSubscription) {
this.blocksSubscription.unsubscribe();
}
this.blocksSubscription = (new Subject<BlockExtended[]>()).subscribe((blocks) => {
this.handleNewBlock(blocks.slice(0, this.numBlocks));
});
this.shiftTestBlocks();
} else if (!this.blocksSubscription) {
this.blocksSubscription = this.stateService.blocks$
.subscribe((blocks) => {
this.handleNewBlock(blocks.slice(0, this.numBlocks));
});
}
});
this.setupBlockGraphs();
this.networkChangedSubscription = this.stateService.networkChanged$
.subscribe((network) => this.network = network);
}
ngAfterViewInit(): void {
this.graphChangeSubscription = this.blockGraphs.changes.pipe(startWith(null)).subscribe(() => {
this.setupBlockGraphs();
});
}
ngOnDestroy(): void {
this.stateService.markBlock$.next({});
if (this.blocksSubscription) {
this.blocksSubscription?.unsubscribe();
}
this.cacheBlocksSubscription?.unsubscribe();
this.networkChangedSubscription?.unsubscribe();
this.queryParamsSubscription?.unsubscribe();
}
shiftTestBlocks(): void {
const sub = this.apiService.getBlocks$(this.testHeight).subscribe(result => {
sub.unsubscribe();
this.handleNewBlock(result.slice(0, this.numBlocks));
this.testHeight++;
clearTimeout(this.testShiftTimeout);
this.testShiftTimeout = window.setTimeout(() => { this.shiftTestBlocks(); }, 10000);
});
}
async handleNewBlock(blocks: BlockExtended[]): Promise<void> {
const readyPromises: Promise<TransactionStripped[]>[] = [];
const previousBlocks = this.latestBlocks;
const newHeights = {};
this.latestBlocks = blocks;
for (const block of blocks) {
newHeights[block.height] = true;
if (!this.strippedTransactions[block.height]) {
readyPromises.push(new Promise((resolve) => {
const subscription = this.apiService.getStrippedBlockTransactions$(block.id).pipe(
catchError(() => {
return of([]);
}),
).subscribe((transactions) => {
this.strippedTransactions[block.height] = transactions;
subscription.unsubscribe();
resolve(transactions);
});
}));
}
}
await Promise.allSettled(readyPromises);
this.updateBlockGraphs(blocks);
// free up old transactions
previousBlocks.forEach(block => {
if (!newHeights[block.height]) {
delete this.strippedTransactions[block.height];
}
});
}
updateBlockGraphs(blocks): void {
const startTime = performance.now() + 1000 - (this.stagger < 0 ? this.stagger * 8 : 0);
if (this.blockGraphs) {
this.blockGraphs.forEach((graph, index) => {
graph.replace(this.strippedTransactions[blocks?.[index]?.height] || [], 'right', false, startTime + (this.stagger * index));
});
}
this.showInfo = false;
setTimeout(() => {
this.blockInfo = blocks.map(block => {
return {
...block,
timeString: (new Date(block.timestamp * 1000)).toLocaleTimeString(),
};
});
this.showInfo = true;
}, 1600); // Should match the animation time.
}
setupBlockGraphs(): void {
if (this.blockGraphs) {
this.blockGraphs.forEach((graph, index) => {
graph.destroy();
graph.setup(this.strippedTransactions[this.latestBlocks?.[index]?.height] || []);
});
}
}
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
if (!event.keyModifier) {
this.router.navigate([url]);
} else {
window.open(url, '_blank');
}
}
onTxHover(txid: string): void {
if (txid && txid.length) {
this.hoverTx = txid;
} else {
this.hoverTx = null;
}
}
}

View File

@@ -0,0 +1,3 @@
.fee-distribution-chart {
margin-top: 0.75rem;
}

View File

@@ -1,4 +1,4 @@
import { OnChanges, OnDestroy } from '@angular/core';
import { HostListener, OnChanges, OnDestroy } from '@angular/core';
import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { TransactionStripped } from '../../interfaces/websocket.interface';
import { StateService } from '../../services/state.service';
@@ -9,6 +9,7 @@ import { Subscription } from 'rxjs';
@Component({
selector: 'app-fee-distribution-graph',
templateUrl: './fee-distribution-graph.component.html',
styleUrls: ['./fee-distribution-graph.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestroy {
@@ -25,6 +26,7 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
simple: boolean = false;
data: number[][];
labelInterval: number = 50;
smallScreen: boolean = window.innerWidth < 450;
rateUnitSub: Subscription;
weightMode: boolean = false;
@@ -95,9 +97,9 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
this.mempoolVsizeFeesOptions = {
grid: {
height: '210',
right: '20',
right: this.smallScreen ? '10' : '20',
top: '22',
left: '40',
left: this.smallScreen ? '10' : '40',
},
xAxis: {
type: 'category',
@@ -131,16 +133,17 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
}
},
axisLabel: {
show: true,
show: !this.smallScreen,
formatter: (value: number): string => {
const unitValue = this.weightMode ? value / 4 : value;
const selectedPowerOfTen = selectPowerOfTen(unitValue);
const newVal = Math.round(unitValue / selectedPowerOfTen.divider);
const scaledValue = unitValue / selectedPowerOfTen.divider;
const newVal = scaledValue >= 100 ? Math.round(scaledValue) : scaledValue.toPrecision(3);
return `${newVal}${selectedPowerOfTen.unit}`;
},
},
axisTick: {
show: true,
show: !this.smallScreen,
}
},
series: [{
@@ -151,11 +154,13 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
position: 'top',
color: '#ffffff',
textShadowBlur: 0,
fontSize: this.smallScreen ? 10 : 12,
formatter: (label: { data: number[] }): string => {
const value = label.data[1];
const unitValue = this.weightMode ? value / 4 : value;
const selectedPowerOfTen = selectPowerOfTen(unitValue);
const newVal = Math.round(unitValue / selectedPowerOfTen.divider);
const scaledValue = unitValue / selectedPowerOfTen.divider;
const newVal = scaledValue >= 100 ? Math.round(scaledValue) : scaledValue.toPrecision(3);
return `${newVal}${selectedPowerOfTen.unit}`;
}
},
@@ -179,6 +184,16 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
};
}
@HostListener('window:resize', ['$event'])
onResize(): void {
const isSmallScreen = window.innerWidth < 450;
if (this.smallScreen !== isSmallScreen) {
this.smallScreen = isSmallScreen;
this.prepareChart();
this.mountChart();
}
}
ngOnDestroy(): void {
this.rateUnitSub.unsubscribe();
}

View File

@@ -11,7 +11,7 @@
<div class="progress inc-tx-progress-bar">
<div class="progress-bar" role="progressbar" [ngStyle]="{'width': mempoolInfoData.progressWidth, 'background-color': mempoolInfoData.progressColor}">&nbsp;</div>
<div class="progress-text" *only-vsize>&lrm;{{ mempoolInfoData.vBytesPerSecond | ceil | number }} <ng-container i18n="shared.vbytes-per-second|vB/s">vB/s</ng-container></div>
<div class="progress-text" *only-weight>&lrm;{{ mempoolInfoData.vBytesPerSecond * 4 | ceil | number }} <ng-container i18n="shared.weight-units-per-second|vB/s">WU/s</ng-container></div>
<div class="progress-text" *only-weight>&lrm;{{ mempoolInfoData.vBytesPerSecond * 4 | ceil | number }} <ng-container i18n="shared.weight-per-second|WU/s">WU/s</ng-container></div>
</div>
</ng-template>
</ng-template>

View File

@@ -22,7 +22,7 @@
<a class="dropdown-item" routerLinkActive="active"
[routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" i18n="mining.block-sizes-weights">Block Sizes and Weights</a>
<a *ngIf="stateService.env.AUDIT" class="dropdown-item" routerLinkActive="active"
[routerLink]="['/graphs/mining/block-health' | relativeUrl]" i18n="mining.block-health">Block Health</a>
[routerLink]="['/graphs/mining/block-health' | relativeUrl]" i18n="mining.blocks-health">Block Health</a>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
import { EChartsOption, graphic } from 'echarts';
import { echarts, EChartsOption } from '../../graphs/echarts';
import { merge, Observable, of } from 'rxjs';
import { map, mergeMap, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
@@ -204,7 +204,7 @@ export class HashrateChartComponent implements OnInit {
title: title,
animation: false,
color: [
new graphic.LinearGradient(0, 0, 0, 0.65, [
new echarts.graphic.LinearGradient(0, 0, 0, 0.65, [
{ offset: 0, color: '#F4511E99' },
{ offset: 0.25, color: '#FB8C0099' },
{ offset: 0.5, color: '#FFB30099' },
@@ -212,7 +212,7 @@ export class HashrateChartComponent implements OnInit {
{ offset: 1, color: '#7CB34299' }
]),
'#D81B60',
new graphic.LinearGradient(0, 0, 0, 0.65, [
new echarts.graphic.LinearGradient(0, 0, 0, 0.65, [
{ offset: 0, color: '#F4511E' },
{ offset: 0.25, color: '#FB8C00' },
{ offset: 0.5, color: '#FFB300' },
@@ -342,7 +342,7 @@ export class HashrateChartComponent implements OnInit {
type: 'value',
axisLabel: {
color: 'rgb(110, 112, 121)',
formatter: (val) => {
formatter: (val): string => {
const selectedPowerOfTen: any = selectPowerOfTen(val);
const newVal = Math.round(val / selectedPowerOfTen.divider);
return `${newVal} ${selectedPowerOfTen.unit}H/s`;
@@ -364,9 +364,9 @@ export class HashrateChartComponent implements OnInit {
position: 'right',
axisLabel: {
color: 'rgb(110, 112, 121)',
formatter: (val) => {
formatter: (val): string => {
if (this.stateService.network === 'signet') {
return val;
return `${val}`;
}
const selectedPowerOfTen: any = selectPowerOfTen(val);
const newVal = Math.round(val / selectedPowerOfTen.divider);

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
import { EChartsOption } from 'echarts';
import { EChartsOption } from '../../graphs/echarts';
import { Observable } from 'rxjs';
import { delay, map, retryWhen, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';

View File

@@ -1,5 +1,5 @@
import { Component, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
import { EChartsOption } from 'echarts';
import { EChartsOption } from '../../graphs/echarts';
import { OnChanges } from '@angular/core';
import { StorageService } from '../../services/storage.service';
import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/graphs.utils';
@@ -7,6 +7,8 @@ import { formatNumber } from '@angular/common';
import { StateService } from '../../services/state.service';
import { Subscription } from 'rxjs';
const OUTLIERS_MEDIAN_MULTIPLIER = 4;
@Component({
selector: 'app-incoming-transactions-graph',
templateUrl: './incoming-transactions-graph.component.html',
@@ -29,6 +31,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
@Input() left: number | string = '0';
@Input() template: ('widget' | 'advanced') = 'widget';
@Input() windowPreferenceOverride: string;
@Input() outlierCappingEnabled: boolean = false;
isLoading = true;
mempoolStatsChartOption: EChartsOption = {};
@@ -40,6 +43,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
MA: number[][] = [];
weightMode: boolean = false;
rateUnitSub: Subscription;
medianVbytesPerSecond: number | undefined;
constructor(
@Inject(LOCALE_ID) private locale: string,
@@ -63,44 +67,53 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
return;
}
this.windowPreference = this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference');
this.MA = this.calculateMA(this.data.series[0]);
const windowSize = Math.max(10, Math.floor(this.data.series[0].length / 8));
this.MA = this.calculateMA(this.data.series[0], windowSize);
if (this.outlierCappingEnabled === true) {
this.computeMedianVbytesPerSecond(this.data.series[0]);
}
this.mountChart();
}
rendered() {
if (!this.data) {
return;
return;
}
this.isLoading = false;
}
/// calculate the moving average of maData
calculateMA(maData): number[][] {
/**
* Calculate the median value of the vbytes per second chart to hide outliers
*/
computeMedianVbytesPerSecond(data: number[][]): void {
const vBytes: number[] = [];
for (const value of data) {
vBytes.push(value[1]);
}
const sorted = vBytes.slice().sort((a, b) => a - b);
const middle = Math.floor(sorted.length / 2);
this.medianVbytesPerSecond = sorted[middle];
if (sorted.length % 2 === 0) {
this.medianVbytesPerSecond = (sorted[middle - 1] + sorted[middle]) / 2;
}
}
/// calculate the moving average of the provided data based on windowSize
calculateMA(data: number[][], windowSize: number = 100): number[][] {
//update const variables that are not changed
const ma: number[][] = [];
let sum = 0;
let i = 0;
const len = maData.length;
//Adjust window length based on the length of the data
//5% appeared as a good amount from tests
//TODO: make this a text box in the UI
const maWindowLen = Math.ceil(len * 0.05);
//calculate the center of the moving average window
const center = Math.floor(maWindowLen / 2);
//calculate the centered moving average
for (i = center; i < len - center; i++) {
sum = 0;
//build out ma as we loop through the data
ma[i] = [];
ma[i].push(maData[i][0]);
for (let j = i - center; j <= i + center; j++) {
sum += maData[j][1];
for (i = 0; i < data.length; i++) {
sum += data[i][1];
if (i >= windowSize) {
sum -= data[i - windowSize][1];
const midpoint = i - Math.floor(windowSize / 2);
const avg = sum / windowSize;
ma.push([data[midpoint][0], avg]);
}
ma[i].push(sum / maWindowLen);
}
//return the moving average array
@@ -138,36 +151,22 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
}
}],
}
},
{
zlevel: 0,
name: 'MA',
data: this.MA,
type: 'line',
smooth: false,
showSymbol: false,
symbol: 'none',
lineStyle: {
width: 1,
color: "white",
},
markLine: {
silent: true,
});
if (this.template !== 'widget') {
seriesGraph.push({
zlevel: 0,
name: 'MA',
data: this.MA,
type: 'line',
smooth: false,
showSymbol: false,
symbol: 'none',
lineStyle: {
color: '#fff',
opacity: 1,
width: 2,
},
data: [{
yAxis: 1667,
label: {
show: false,
color: '#ffffff',
}
}],
}
});
color: "white",
}
});
}
this.mempoolStatsChartOption = {
grid: {
@@ -256,6 +255,13 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
}
],
yAxis: {
max: (value) => {
if (!this.outlierCappingEnabled || value.max < this.medianVbytesPerSecond * OUTLIERS_MEDIAN_MULTIPLIER) {
return undefined;
} else {
return Math.round(this.medianVbytesPerSecond * OUTLIERS_MEDIAN_MULTIPLIER);
}
},
type: 'value',
axisLabel: {
fontSize: 11,

View File

@@ -1,6 +1,6 @@
import { Component, Inject, LOCALE_ID, ChangeDetectionStrategy, Input, OnChanges, OnInit } from '@angular/core';
import { formatDate, formatNumber } from '@angular/common';
import { EChartsOption } from 'echarts';
import { EChartsOption } from '../../graphs/echarts';
@Component({
selector: 'app-lbtc-pegs-graph',

View File

@@ -1,5 +1,5 @@
<ng-container *ngIf="{ val: network$ | async } as network">
<header>
<header class="sticky-header">
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">

View File

@@ -1,3 +1,11 @@
.sticky-header {
position: sticky;
position: -webkit-sticky;
top: 0;
width: 100%;
z-index: 100;
}
li.nav-item.active {
background-color: #653b9c;
}

View File

@@ -5,7 +5,7 @@
<!-- Hamburger -->
<ng-container *ngIf="servicesEnabled">
<div *ngIf="user" class="profile_image_container" [class]="{'anon': !user.imageMd5}" (click)="hamburgerClick($event)">
<img *ngIf="user.imageMd5" [src]="'/api/v1/services/account/image/' + user.username + '?md5=' + user.imageMd5" class="profile_image">
<img *ngIf="user.imageMd5" [src]="'/api/v1/services/account/images/' + user.username + '?md5=' + user.imageMd5" class="profile_image">
<app-svg-images style="color: lightgrey; fill: lightgray" *ngIf="!user.imageMd5" name="anon"></app-svg-images>
</div>
<div *ngIf="false && user === null" class="profile_image_container" (click)="hamburgerClick($event)">
@@ -18,7 +18,7 @@
<a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
<ng-template [ngIf]="subdomain">
<div class="subdomain_container">
<img [src]="'/api/v1/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo">
<img [src]="'/api/v1/services/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo">
</div>
</ng-template>
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
@@ -71,13 +71,14 @@
<a class="nav-link" [routerLink]="['/about']" (click)="collapse()"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" i18n-title="master-page.about" title="About"></fa-icon></a>
</li>
</ul>
<app-search-form class="search-form-container" location="top" (searchTriggered)="collapse()"></app-search-form>
<app-search-form [hamburgerOpen]="user != null" class="search-form-container" location="top" (searchTriggered)="collapse()"></app-search-form>
</div>
</nav>
</header>
<div class="d-flex" style="overflow: clip">
<app-menu *ngIf="servicesEnabled" [navOpen]="menuOpen" (loggedOut)="onLoggedOut()" (menuToggled)="menuToggled($event)"></app-menu>
<div *ngIf="!servicesEnabled" class="sidenav"><!-- empty sidenav needed to push footer down the screen --></div>
<div class="flex-grow-1 d-flex flex-column">
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert>

View File

@@ -238,4 +238,15 @@ nav {
main {
transition: 0.2s;
transition-property: max-width;
}
}
// empty sidenav
.sidenav {
z-index: 1;
background-color: transparent;
width: 0px;
height: calc(100vh - 65px);
position: sticky;
top: 65px;
padding-bottom: 20px;
}

View File

@@ -0,0 +1,5 @@
<div class="block-wrapper">
<div class="block-container">
<app-mempool-block-overview [index]="index"></app-mempool-block-overview>
</div>
</div>

View File

@@ -0,0 +1,22 @@
.block-wrapper {
width: 100vw;
height: 100vh;
background: #181b2d;
}
.block-container {
flex-grow: 0;
flex-shrink: 0;
width: 100vw;
max-width: 100vh;
height: 100vh;
padding: 0;
margin: auto;
display: flex;
justify-content: center;
align-items: center;
* {
flex-grow: 1;
}
}

View File

@@ -0,0 +1,85 @@
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Subscription, filter, map, switchMap, tap } from 'rxjs';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
function bestFitResolution(min, max, n): number {
const target = (min + max) / 2;
let bestScore = Infinity;
let best = null;
for (let i = min; i <= max; i++) {
const remainder = (n % i);
if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) {
bestScore = remainder;
best = i;
}
}
return best;
}
@Component({
selector: 'app-mempool-block-view',
templateUrl: './mempool-block-view.component.html',
styleUrls: ['./mempool-block-view.component.scss']
})
export class MempoolBlockViewComponent implements OnInit, OnDestroy {
autofit: boolean = false;
resolution: number = 80;
index: number = 0;
routeParamsSubscription: Subscription;
queryParamsSubscription: Subscription;
constructor(
private route: ActivatedRoute,
private websocketService: WebsocketService,
public stateService: StateService,
) { }
ngOnInit(): void {
this.websocketService.want(['blocks', 'mempool-blocks']);
this.routeParamsSubscription = this.route.paramMap
.pipe(
switchMap((params: ParamMap) => {
this.index = parseInt(params.get('index'), 10) || 0;
return this.stateService.mempoolBlocks$
.pipe(
map((blocks) => {
if (!blocks.length) {
return [{ index: 0, blockSize: 0, blockVSize: 0, feeRange: [0, 0], medianFee: 0, nTx: 0, totalFees: 0 }];
}
return blocks;
}),
filter((mempoolBlocks) => mempoolBlocks.length > 0),
tap((mempoolBlocks) => {
while (!mempoolBlocks[this.index]) {
this.index--;
}
})
);
})
).subscribe();
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
this.autofit = params.autofit === 'true';
if (this.autofit) {
this.onResize();
}
});
}
@HostListener('window:resize', ['$event'])
onResize(): void {
if (this.autofit) {
this.resolution = bestFitResolution(64, 96, Math.min(window.innerWidth, window.innerHeight));
}
}
ngOnDestroy(): void {
this.routeParamsSubscription.unsubscribe();
this.queryParamsSubscription.unsubscribe();
}
}

View File

@@ -6,7 +6,7 @@ import { formatNumber } from '@angular/common';
import { OptimizedMempoolStats } from '../../interfaces/node-api.interface';
import { StateService } from '../../services/state.service';
import { StorageService } from '../../services/storage.service';
import { EChartsOption } from 'echarts';
import { EChartsOption } from '../../graphs/echarts';
import { feeLevels, chartColors } from '../../app.constants';
import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/graphs.utils';
@@ -27,7 +27,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
@Input() data: any[];
@Input() filterSize = 100000;
@Input() limitFilterFee = 1;
@Input() hideCount: boolean = false;
@Input() hideCount: boolean = true;
@Input() height: number | string = 200;
@Input() top: number | string = 20;
@Input() right: number | string = 10;
@@ -53,7 +53,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
chartInstance: any = undefined;
weightMode: boolean = false;
isWidget: boolean = false;
showCount: boolean = true;
showCount: boolean = false;
constructor(
private vbytesPipe: VbytesPipe,
@@ -230,7 +230,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
positions[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 100;
return positions;
},
extraCssText: `width: ${(this.template === 'advanced') ? '275px' : '200px'};
extraCssText: `width: ${(this.template === 'advanced') ? '300px' : '200px'};
background: transparent;
border: none;
box-shadow: none;`,
@@ -254,7 +254,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, params[0].axisValue);
const { totalValue, totalValueArray } = this.getTotalValues(params);
const itemFormatted = [];
let totalParcial = 0;
let sum = 0;
let progressPercentageText = '';
let countItem;
let items = this.inverted ? [...params].reverse() : params;
@@ -262,7 +262,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
countItem = items.pop();
}
items.map((item: any, index: number) => {
totalParcial += item.value[1];
sum += item.value[1];
const progressPercentage = (item.value[1] / totalValue) * 100;
const progressPercentageSum = (totalValueArray[index] / totalValue) * 100;
let activeItemClass = '';
@@ -279,7 +279,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
<span class="symbol">%</span>
</span>
<span class="total-parcial-vbytes">
${this.vbytesPipe.transform(totalParcial, 2, 'vB', 'MvB', false)}
${this.vbytesPipe.transform(sum, 2, 'vB', 'MvB', false)}
</span>
<div class="total-percentage-bar">
<span class="total-percentage-bar-background">
@@ -303,12 +303,12 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
</td>
<td class="total-progress-sum">
<span>
${this.vbytesPipe.transform(item.value[1], 2, 'vB', 'MvB', false)}
${(item.value[1] / 1_000_000).toFixed(2)} <span class="symbol">MvB</span>
</span>
</td>
<td class="total-progress-sum">
<span>
${this.vbytesPipe.transform(totalValueArray[index], 2, 'vB', 'MvB', false)}
${(totalValueArray[index] / 1_000_000).toFixed(2)} <span class="symbol">MvB</span>
</span>
</td>
<td class="total-progress-sum-bar">

View File

@@ -3,11 +3,11 @@
<nav class="scrollable menu-click">
<span *ngIf="userAuth" class="menu-click">
<strong class="menu-click">@ {{ userAuth.user.username }}</strong>
<strong class="menu-click text-nowrap ellipsis">@ {{ userAuth.user.username }}</strong>
</span>
<a *ngIf="!userAuth" class="d-flex justify-content-center align-items-center nav-link m-0 menu-click" routerLink="/login" role="tab" (click)="onLinkClick('/login')">
<fa-icon class="menu-click" [icon]="['fas', 'user-circle']" [fixedWidth]="true" style="font-size: 25px;margin-right: 15px;"></fa-icon>
<span class="menu-click" style="font-size: 20px;">Sign in</span>
<span class="menu-click" style="font-size: 20px;" i18n="shared.sign-in">Sign in</span>
</a>
<ng-container *ngIf="userMenuGroups$ | async as menuGroups">

View File

@@ -9,17 +9,27 @@
margin-left: -250px;
box-shadow: 5px 0px 30px 0px #000;
padding-bottom: 20px;
@media (max-width: 613px) {
top: 105px;
}
}
.ellipsis {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
.scrollable {
overflow-x: hidden;
overflow-y: scroll;
overflow-y: auto;
}
.sidenav.open {
margin-left: 0px;
left: 0px;
display: block;
background-color: #1d1f31;
}
.sidenav a, button{

View File

@@ -44,12 +44,12 @@
</div>
</div>
<!-- Latest blocks -->
<!-- Recent blocks -->
<div class="col">
<div class="card">
<div class="card-body">
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
<h5 class="card-title d-inline" i18n="dashboard.recent-blocks">Recent Blocks</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>

View File

@@ -6,7 +6,7 @@
<div class="pool-distribution" *ngIf="(miningStatsObservable$ | async) as miningStats; else loadingReward">
<div class="item">
<h5 class="card-title d-inline-block" i18n="mining.miners-luck" i18n-ngbTooltip="mining.miners-luck-1w"
ngbTooltip="Pools luck (1 week)" placement="bottom" #minersluck [disableTooltip]="!isEllipsisActive(minersluck)">Pools luck</h5>
ngbTooltip="Pools luck (1 week)" placement="bottom" #minersluck [disableTooltip]="!isEllipsisActive(minersluck)">Pools Luck</h5>
<p class="card-text" i18n-ngbTooltip="mining.pools-luck-desc"
ngbTooltip="The overall luck of all mining pools over the past week. A luck bigger than 100% means the average block time for the current epoch is less than 10 minutes." placement="bottom">
{{ miningStats['minersLuck'] }}%
@@ -14,14 +14,14 @@
</div>
<div class="item">
<h5 class="card-title d-inline-block" i18n="mining.miners-count" i18n-ngbTooltip="mining.miners-count-1w"
ngbTooltip="Pools count (1w)" placement="bottom" #poolscount [disableTooltip]="!isEllipsisActive(poolscount)">Pools count</h5>
ngbTooltip="Pools count (1w)" placement="bottom" #poolscount [disableTooltip]="!isEllipsisActive(poolscount)">Pools Count</h5>
<p class="card-text" i18n-ngbTooltip="mining.pools-count-desc"
ngbTooltip="How many unique pools found at least one block over the past week." placement="bottom">
{{ miningStats.pools.length }}
</p>
</div>
<div class="item">
<h5 class="card-title d-inline-block" i18n="master-page.blocks" i18n-ngbTooltip="master-page.blocks"
<h5 class="card-title d-inline-block" i18n="shared.blocks-1w" i18n-ngbTooltip="master-page.blocks"
ngbTooltip="Blocks (1w)" placement="bottom" #blockscount [disableTooltip]="!isEllipsisActive(blockscount)">Blocks (1w)</h5>
<p class="card-text" i18n-ngbTooltip="mining.blocks-count-desc"
ngbTooltip="The number of blocks found over the past week." placement="bottom">
@@ -95,7 +95,7 @@
<th *ngIf="auditAvailable" class="health text-right widget" i18n="latest-blocks.avg_health"
i18n-ngbTooltip="latest-blocks.avg_health" ngbTooltip="Avg Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Avg Health</th>
<th *ngIf="auditAvailable" class="d-none d-sm-table-cell" i18n="mining.fees-per-block">Avg Block Fees</th>
<th class="d-none d-lg-table-cell" i18n="mining.empty-blocks">Empty blocks</th>
<th class="d-none d-lg-table-cell" i18n="mining.empty-blocks">Empty Blocks</th>
</tr>
</thead>
<tbody [attr.data-cy]="'pools-table'" *ngIf="(miningStatsObservable$ | async) as miningStats">
@@ -153,19 +153,19 @@
<ng-template #loadingReward>
<div class="pool-distribution">
<div class="item">
<h5 class="card-title" i18n="mining.miners-luck">Pools Luck (1w)</h5>
<h5 class="card-title" i18n="mining.miners-luck">Pools Luck</h5>
<p class="card-text">
<span class="skeleton-loader skeleton-loader-big"></span>
</p>
</div>
<div class="item">
<h5 class="card-title" i18n="master-page.blocks">Blocks (1w)</h5>
<h5 class="card-title" i18n="mining.miners-count" >Pools Count</h5>
<p class="card-text">
<span class="skeleton-loader skeleton-loader-big"></span>
</p>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.miners-count">Pools Count (1w)</h5>
<h5 class="card-title" i18n="shared.blocks-1w">Blocks (1w)</h5>
<p class="card-text">
<span class="skeleton-loader skeleton-loader-big"></span>
</p>

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit, HostBinding } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { EChartsOption, PieSeriesOption } from 'echarts';
import { EChartsOption, PieSeriesOption } from '../../graphs/echarts';
import { merge, Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { SeoService } from '../../services/seo.service';

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { EChartsOption, graphic } from 'echarts';
import { echarts, EChartsOption } from '../../graphs/echarts';
import { Observable, of } from 'rxjs';
import { map, switchMap, catchError } from 'rxjs/operators';
import { PoolStat } from '../../interfaces/node-api.interface';
@@ -127,7 +127,7 @@ export class PoolPreviewComponent implements OnInit {
title: title,
animation: false,
color: [
new graphic.LinearGradient(0, 0, 0, 0.65, [
new echarts.graphic.LinearGradient(0, 0, 0, 0.65, [
{ offset: 0, color: '#F4511E' },
{ offset: 0.25, color: '#FB8C00' },
{ offset: 0.5, color: '#FFB300' },

View File

@@ -143,7 +143,7 @@
<table class="table table-xs table-data">
<thead>
<tr>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks (24h)</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
</tr>
@@ -165,7 +165,7 @@
<table class="table table-xs table-data">
<thead>
<tr>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks (24h)</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
</tr>
@@ -433,7 +433,7 @@
<table class="table table-xs table-data text-center">
<thead>
<tr>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks (24h)</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
</tr>
@@ -458,7 +458,7 @@
<table class="table table-xs table-data text-center">
<thead>
<tr>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks (24h)</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
</tr>

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { EChartsOption, graphic } from 'echarts';
import { echarts, EChartsOption } from '../../graphs/echarts';
import { BehaviorSubject, Observable, of, timer } from 'rxjs';
import { catchError, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators';
import { BlockExtended, PoolStat } from '../../interfaces/node-api.interface';
@@ -131,7 +131,7 @@ export class PoolComponent implements OnInit {
title: title,
animation: false,
color: [
new graphic.LinearGradient(0, 0, 0, 0.65, [
new echarts.graphic.LinearGradient(0, 0, 0, 0.65, [
{ offset: 0, color: '#F4511E' },
{ offset: 0.25, color: '#FB8C00' },
{ offset: 0.5, color: '#FFB300' },

View File

@@ -5,7 +5,7 @@
<br><br>
<h2>Privacy Policy</h2>
<h6>Updated: November 18, 2021</h6>
<h6>Updated: November 23, 2023</h6>
<br><br>
@@ -53,6 +53,26 @@
<br>
<h4>SIGNING UP FOR AN ACCOUNT ON MEMPOOL.SPACE</h4>
<p>If you sign up for an account on mempool.space, we may collect the following:</p>
<ol>
<li>If you provide your name, country, and/or e-mail address, we may use this information to manage your user account, for billing purposes, or to update you about our services. We will not share this with any third-party, except as detailed below if you sponsor The Mempool Open Source Project®, purchase a subscription to Mempool Enterprise®, or accelerate transactions using Mempool Accelerator™.</li>
<li>If you connect your Twitter account, we may store your Twitter identity, e-mail address, and profile photo. We may publicly display your profile photo or link to your profile on our website, if you sponsor The Mempool Open Source Project, claim your Lightning node, or other such use cases.</li>
<li>If you make a credit card payment, we will process your payment using Square (Block, Inc.), and we will store details about the transaction in our database. Please see "Information we collect about customers" on Square's website at https://squareup.com/us/en/legal/general/privacy</li>
<li>If you make a Bitcoin or Liquid payment, we will process your payment using our self-hosted BTCPay Server instance and not share these details with any third-party.</li>
<li>If you accelerate transactions using Mempool Accelerator™, we will store the TXID of your transactions you accelerate with us. We share this information with our mining pool partners, as well as publicly display accelerated transaction details on our website and APIs.</li>
</ol>
<br>
<p>EOF</p>
</div>

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