Compare commits

...

372 Commits

Author SHA1 Message Date
orangesurf
c08c206c8b Add initial accelerator FAQ entries 2023-12-06 17:31:07 +00:00
wiz
51a28b2e01 Merge pull request #4480 from mempool/mononaut/mined-acceleration-info
Show accelerated fee rates for mined transactions
2023-12-05 23:00:15 +09:00
Mononaut
966adf5963 Show accelerated fee rates on mined tx pages 2023-12-05 13:48:38 +00:00
wiz
ae7b17c4fd Merge pull request #4478 from mempool/mononaut/accelerated-badge
Accelerated tx audit badge
2023-12-05 17:36:52 +09:00
Mononaut
5b4bc9fe19 Accelerated tx audit badge 2023-12-05 08:33:57 +00:00
wiz
89eb7ec90b ops: Enable accelerator in production config 2023-12-05 05:29:18 +09:00
wiz
a646c5f22f Merge pull request #4390 from ncois/tooltip-overflow-bug
Fix tooltip overflow by increasing width
2023-12-04 23:44:44 +09:00
wiz
c345f2164b Merge pull request #4462 from ncois/search-block-datetime
Search a block by date and timestamp
2023-12-04 23:44:19 +09:00
wiz
9791ee018d Merge pull request #4466 from ncois/pool-hashrate-format
Pool chart hashrate format in power of ten
2023-12-04 23:44:06 +09:00
wiz
7c83b85e3e Merge pull request #4476 from mempool/nymkappa/fix-waitlist-message
[accel preview] only show waitlist message if logged in
2023-12-04 23:43:50 +09:00
nymkappa
63942af720 wait list -> waitlist @softsimon 2023-12-04 23:30:22 +09:00
nymkappa
4da5910e0b [accel preview] only show waitlist message if logged in 2023-12-04 23:23:51 +09:00
wiz
025788e78c Merge pull request #4471 from mempool/nymkappa/fix-one-bug-add-two-more
Various fixes and improvements
2023-12-04 22:51:41 +09:00
nymkappa
88a9524064 Merge branch 'master' into pool-hashrate-format 2023-12-04 21:34:43 +09:00
ncois
c216e849e6 Add more precision to hashrate graphs 2023-12-04 09:56:56 +01:00
nymkappa
c86768edc9 [about] fix merge conflict #4425 2023-12-04 16:59:29 +09:00
nymkappa
4283d30ce0 Merge branch 'master' into search-block-datetime 2023-12-04 13:09:58 +09:00
nymkappa
e2eeaebfc7 Merge branch 'master' into nymkappa/fix-one-bug-add-two-more 2023-12-04 10:47:49 +09:00
softsimon
90d87aaaca Merge pull request #4472 from mempool/simon/upgrading-fontawesome
Upgrading fontawesome
2023-12-03 18:17:33 +09:00
softsimon
b2e9275ad9 Upgrading fontawesome 2023-12-03 17:34:31 +09:00
nymkappa
181e816c1a Merge branch 'nymkappa/about-page-empty-anchor' into nymkappa/fix-one-bug-add-two-more 2023-12-03 15:17:04 +09:00
softsimon
758ca4e93a Merge pull request #4233 from mempool/hunicus/enterprise-abs
Make enterprise links not relative
2023-12-03 14:09:25 +09:00
softsimon
a33f915c7a Merge pull request #4438 from mempool/mononaut/refactor-difficulty-reindexing
Refactor difficulty reindexing to process blocks in height order
2023-12-03 13:15:01 +09:00
softsimon
75c6d006ff Merge pull request #4433 from mempool/mononaut/dismiss-acceleration-preview
Make the acceleration preview dismissable
2023-12-02 18:19:46 +09:00
softsimon
1fe983a299 Merge pull request #4468 from mempool/mononaut/fix-liquid-fee-rounding
Fix liquid recommended fee rounding
2023-12-02 15:05:13 +09:00
Mononaut
cd8e3e2604 Fix liquid fee rounding 2023-12-02 03:20:09 +00:00
wiz
dfc4309d9e Merge pull request #4465 from mempool/mononaut/fix-liquid-minfee
Fix Liquid minfee defaults
2023-12-01 21:52:25 +09:00
ncois
0a70273456 Pool chart: show power of ten of hashrate on desktop 2023-12-01 12:57:30 +01:00
softsimon
62c9a88235 Merge pull request #4448 from mempool/nymkappa/accel-preview-logged-in
[accelerator] show wait list message in preview when logged in with no access
2023-12-01 18:37:05 +09:00
Mononaut
851e07b50b Fix liquid minfee defaults 2023-12-01 18:12:59 +09:00
wiz
e1bbec074b Merge pull request #4457 from mempool/mononaut/matomo-path
matomo path
2023-12-01 17:54:06 +09:00
Mononaut
9fcafeeeb0 Switch to toggle-style acceleration dismiss button 2023-12-01 14:55:19 +09:00
Mononaut
e986cfd30b Make the acceleration preview dismissable 2023-12-01 14:21:05 +09:00
ncois
2584a1f2b0 Add date and timestamp search option 2023-11-30 20:05:25 +01:00
softsimon
1c92394563 Merge pull request #4449 from mempool/nymkappa/translation-typo
Fix some SEO strings
2023-11-30 18:33:05 +09:00
softsimon
36398ca57a Updating messages.xlf 2023-11-30 18:29:06 +09:00
softsimon
6b978f9262 Merge pull request #3706 from mempool/mononaut/reconnect-dead-websocket
reconnect websocket after closed by server
2023-11-30 18:26:18 +09:00
softsimon
12cf130c00 Merge branch 'master' into mononaut/reconnect-dead-websocket 2023-11-30 17:57:19 +09:00
softsimon
ba933a81c3 Merge pull request #3704 from mempool/mononaut/handle-websocket-errors
Disconnect websocket clients on error
2023-11-30 17:56:58 +09:00
Mononaut
7de7081d67 matomo path 2023-11-30 17:35:26 +09:00
nymkappa
6b933c202f Update nodes-rankings-dashboard.component.ts 2023-11-30 17:22:22 +09:00
softsimon
f1525b7df5 Merge pull request #4445 from mempool/dependabot/npm_and_yarn/frontend/fortawesome/fontawesome-svg-core-6.5.0
Bump @fortawesome/fontawesome-svg-core from 6.4.0 to 6.5.0 in /frontend
2023-11-30 17:05:19 +09:00
wiz
62c4af1a0c Merge pull request #4455 from mempool/mononaut/fix-matomo-fix
Fix matomo again
2023-11-30 15:49:00 +09:00
Mononaut
4ac98bc483 Fix matomo again 2023-11-30 15:46:58 +09:00
softsimon
ed07daebca Merge branch 'master' into mononaut/handle-websocket-errors 2023-11-30 15:18:16 +09:00
wiz
f8bbf7783c Merge pull request #4443 from mempool/nymkappa/debug-add-context
[log] create more general error
2023-11-30 14:53:51 +09:00
softsimon
fac2fab16a Merge pull request #4437 from mempool/simon/remove-audit-beta
Remove block audit beta-tag
2023-11-30 14:50:57 +09:00
wiz
76de4e34d8 Merge pull request #4425 from mempool/mononaut/fix-matomo
fix matomo bug
2023-11-30 14:40:26 +09:00
softsimon
6109f25aa9 Merge pull request #4452 from ncois/tx-ui-overflow
Fix overflow on transaction page
2023-11-30 14:36:15 +09:00
softsimon
86303175e9 Merge pull request #4451 from ncois/format-difficulty-chart
Difficulty adjustment chart: format the hashrate and difficulty
2023-11-30 14:27:39 +09:00
nymkappa
a670131f40 Merge branch 'master' into tooltip-overflow-bug 2023-11-30 11:54:16 +09:00
ncois
c5ce3167f3 Fix overflow on transaction page 2023-11-29 15:27:45 +01:00
ncois
9913254a5c Difficulty value: remove rounding 2023-11-29 11:16:48 +01:00
ncois
fbb4ba39c2 Mining chart: show hashrate power of ten on desktop 2023-11-29 11:08:38 +01:00
nymkappa
22bf0fe928 Re-apply fix from https://github.com/mempool/mempool/pull/4419/files#diff-9daf43654b4e0bb2c925b6396c6e3a7e1b117bd854fe9c46290f025bfe0a762eR285 2023-11-29 18:03:23 +09:00
nymkappa
3b30765070 Revert unnecessary changes 2023-11-29 18:00:07 +09:00
nymkappa
63fde7d0b6 Merge branch 'master' into nymkappa/about-page-sponsors-component 2023-11-29 17:57:54 +09:00
softsimon
cf338970bf Merge pull request #4450 from mempool/translations_frontend-src-locale-messages-xlf--master_fr
Updates for file frontend/src/locale/messages.xlf in fr
2023-11-29 16:35:27 +09:00
Mononaut
08e046ea2a fix matomo bug 2023-11-29 16:17:07 +09:00
transifex-integration[bot]
5697710c23 Translate frontend/src/locale/messages.xlf in fr
100% reviewed source file: 'frontend/src/locale/messages.xlf'
on 'fr'.
2023-11-29 07:00:03 +00:00
nymkappa
b9e050e24b Fix small typo in lightning node ranking dashboard seo 2023-11-29 15:59:15 +09:00
nymkappa
ff819673e5 Fix "and" chain in lightning dashboard seo 2023-11-29 15:56:35 +09:00
nymkappa
2e2801a4ab Add missing txid in seo tx preview 2023-11-29 15:51:35 +09:00
nymkappa
f52b17ca7a Add missing space in seo address preview 2023-11-29 15:28:14 +09:00
softsimon
3d25235705 Merge pull request #4439 from mempool/mononaut/fix-ln-search-suggestions
Fix spurious ln channel typeahead matches
2023-11-29 15:24:34 +09:00
nymkappa
2cbc6783a4 [accelerator] show wait list message in preview when logged in with no access 2023-11-29 15:02:48 +09:00
dependabot[bot]
406bf5d3b7 Bump @fortawesome/fontawesome-svg-core from 6.4.0 to 6.5.0 in /frontend
Bumps [@fortawesome/fontawesome-svg-core](https://github.com/FortAwesome/Font-Awesome) from 6.4.0 to 6.5.0.
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/6.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/6.4.0...6.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-29 02:48:50 +00:00
nymkappa
f5bf883de9 [about] fix auto scroll to chad/whale 2023-11-28 18:05:18 +09:00
nymkappa
c06cade1ca [log] create more general error 2023-11-28 17:13:52 +09:00
softsimon
c23a872887 Updating german i18n 2023-11-28 14:42:25 +09:00
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
Mononaut
0b449347c1 Fix spurious ln channel typeahead matches 2023-11-26 09:22:44 +00: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
d2da81e039 Remove block audit beta-tag 2023-11-26 17:21:13 +09:00
Mononaut
66d88abdc5 Refactor difficulty reindexing to process blocks in height order 2023-11-26 08:17:30 +00: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
Mononaut
85c9c79699 reconnect websocket after closed by server 2023-11-25 11:25:07 +00: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
Mononaut
652100f774 More verbose websocket error logs 2023-11-25 09:02:27 +00:00
Mononaut
0f04f751e1 Disconnect websocket clients on error 2023-11-25 08:27:17 +00:00
wiz
d3055dab54 Merge pull request #4157 from mempool/hunicus/bimi-svg
Add bimi svg
2023-11-25 16:29:29 +09:00
ncois
643402c046 Merge branch 'mempool:master' into tooltip-overflow-bug 2023-11-24 13:28:42 +01:00
nymkappa
e95d5a7982 [lightning] enlarge channel map 2023-11-24 18:14:16 +09:00
nymkappa
3103ef15e5 [about page] move CTA sponsors box into a sub component 2023-11-24 11:13:12 +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
ncois
f57436f511 Merge branch 'master' into tooltip-overflow-bug 2023-11-22 12:14:03 +01: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
ncois
1eb425897e Adjust the width of the incoming tx tooltip for 2h timeframe 2023-11-19 16:42:09 +01: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
ncois
9d0bfdfa88 Increase tooltip width in mempool graphs 2023-11-17 13:47:28 +01: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
hunicus
d630e7217a Make enterprise links not relative 2023-09-06 01:42:19 +09: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
236 changed files with 108108 additions and 69987 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

@@ -80,7 +80,13 @@ class ChannelsApi {
public async $searchChannelsById(search: string): Promise<any[]> {
try {
const searchStripped = search.replace(/[^0-9x]/g, '') + '%';
// restrict search to valid id/short_id prefix formats
let searchStripped = search.match(/[0-9]+[0-9x]*/)?.[0] || '';
if (!searchStripped.length) {
return [];
}
// add wildcard to search by prefix
searchStripped += '%';
const query = `SELECT id, short_id, capacity, status FROM channels WHERE id LIKE ? OR short_id LIKE ? LIMIT 10`;
const [rows]: any = await DB.query(query, [searchStripped, searchStripped]);
return rows;

View File

@@ -3,21 +3,31 @@ 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;
minimumIncrement = 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 minimumFee = this.roundUpToNearest(mPool.mempoolminfee * 100000, this.minimumIncrement);
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 +37,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,
};
}
@@ -45,7 +59,11 @@ class FeeApi {
const multiplier = (pBlock.blockVSize - 500000) / 500000;
return Math.max(Math.round(useFee * multiplier), this.defaultFee);
}
return Math.ceil(useFee);
return this.roundUpToNearest(useFee, this.minimumIncrement);
}
private roundUpToNearest(value: number, nearest: number): number {
return Math.ceil(value / nearest) * nearest;
}
}

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

@@ -18,7 +18,7 @@ class Mempool {
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
private spendMap = new Map<string, MempoolTransactionExtended>();
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
@@ -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

@@ -15,6 +15,13 @@ import bitcoinApi from '../bitcoin/bitcoin-api-factory';
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
import database from '../../database';
interface DifficultyBlock {
timestamp: number,
height: number,
bits: number,
difficulty: number,
}
class Mining {
private blocksPriceIndexingRunning = false;
public lastHashrateIndexingDate: number | null = null;
@@ -421,6 +428,7 @@ class Mining {
indexedHeights[height] = true;
}
// gets {time, height, difficulty, bits} of blocks in ascending order of height
const blocks: any = await BlocksRepository.$getBlocksDifficulty();
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
let currentDifficulty = genesisBlock.difficulty;
@@ -436,41 +444,45 @@ class Mining {
});
}
const oldestConsecutiveBlock = await BlocksRepository.$getOldestConsecutiveBlock();
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== -1) {
currentBits = oldestConsecutiveBlock.bits;
currentDifficulty = oldestConsecutiveBlock.difficulty;
if (!blocks?.length) {
// no blocks in database yet
return;
}
const oldestConsecutiveBlock = this.getOldestConsecutiveBlock(blocks);
currentBits = oldestConsecutiveBlock.bits;
currentDifficulty = oldestConsecutiveBlock.difficulty;
let totalBlockChecked = 0;
let timer = new Date().getTime() / 1000;
for (const block of blocks) {
// skip until the first block after the oldest consecutive block
if (block.height <= oldestConsecutiveBlock.height) {
continue;
}
// difficulty has changed between two consecutive blocks!
if (block.bits !== currentBits) {
if (indexedHeights[block.height] === true) { // Already indexed
if (block.height >= oldestConsecutiveBlock.height) {
currentDifficulty = block.difficulty;
currentBits = block.bits;
}
continue;
// skip if already indexed
if (indexedHeights[block.height] !== true) {
let adjustment = block.difficulty / currentDifficulty;
adjustment = Math.round(adjustment * 1000000) / 1000000; // Remove float point noise
await DifficultyAdjustmentsRepository.$saveAdjustments({
time: block.time,
height: block.height,
difficulty: block.difficulty,
adjustment: adjustment,
});
totalIndexed++;
}
let adjustment = block.difficulty / currentDifficulty;
adjustment = Math.round(adjustment * 1000000) / 1000000; // Remove float point noise
await DifficultyAdjustmentsRepository.$saveAdjustments({
time: block.time,
height: block.height,
difficulty: block.difficulty,
adjustment: adjustment,
});
totalIndexed++;
if (block.height >= oldestConsecutiveBlock.height) {
currentDifficulty = block.difficulty;
currentBits = block.bits;
}
}
// update the current difficulty
currentDifficulty = block.difficulty;
currentBits = block.bits;
}
totalBlockChecked++;
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
@@ -633,6 +645,17 @@ class Mining {
default: return 86400 * scale;
}
}
// Finds the oldest block in a consecutive chain back from the tip
// assumes `blocks` is sorted in ascending height order
private getOldestConsecutiveBlock(blocks: DifficultyBlock[]): DifficultyBlock {
for (let i = blocks.length - 1; i > 0; i--) {
if ((blocks[i].height - blocks[i - 1].height) > 1) {
return blocks[i];
}
}
return blocks[0];
}
}
export default new Mining();

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

@@ -94,9 +94,13 @@ class WebsocketHandler {
throw new Error('WebSocket.Server is not set');
}
this.wss.on('connection', (client: WebSocket) => {
this.wss.on('connection', (client: WebSocket, req) => {
this.numConnected++;
client.on('error', logger.info);
client['remoteAddress'] = req.headers['x-forwarded-for'] || req.socket?.remoteAddress || 'unknown';
client.on('error', (e) => {
logger.info(`websocket client error from ${client['remoteAddress']}: ` + (e instanceof Error ? e.message : e));
client.close();
});
client.on('close', () => {
this.numDisconnected++;
});
@@ -282,7 +286,8 @@ class WebsocketHandler {
client.send(serializedResponse);
}
} catch (e) {
logger.debug('Error parsing websocket message: ' + (e instanceof Error ? e.message : e));
logger.debug(`Error parsing websocket message from ${client['remoteAddress']}: ` + (e instanceof Error ? e.message : e));
client.close();
}
});
});
@@ -577,7 +582,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

@@ -541,7 +541,7 @@ class BlocksRepository {
*/
public async $getBlocksDifficulty(): Promise<object[]> {
try {
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty, bits FROM blocks`);
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty, bits FROM blocks ORDER BY height ASC`);
return rows;
} catch (e) {
logger.err('Cannot get blocks difficulty list from the db. Reason: ' + (e instanceof Error ? e.message : e));

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

File diff suppressed because it is too large Load Diff

View File

@@ -74,9 +74,9 @@
"@angular/platform-server": "^16.2.2",
"@angular/router": "^16.2.2",
"@fortawesome/angular-fontawesome": "~0.13.0",
"@fortawesome/fontawesome-common-types": "~6.4.0",
"@fortawesome/fontawesome-svg-core": "~6.4.0",
"@fortawesome/free-solid-svg-icons": "~6.4.0",
"@fortawesome/fontawesome-common-types": "~6.5.1",
"@fortawesome/fontawesome-svg-core": "~6.5.1",
"@fortawesome/free-solid-svg-icons": "~6.5.1",
"@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^15.1.0",
"@types/qrcode": "~1.5.0",
@@ -85,13 +85,12 @@
"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",
"tinyify": "^4.0.0",
"tinyify": "^3.1.0",
"tlite": "^0.1.9",
"tslib": "~2.6.0",
"zone.js": "~0.13.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

@@ -225,7 +225,7 @@ const witnessSize = (vin: Vin) => vin.witness ? vin.witness.reduce((S, w) => S +
const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0;
// Power of ten wrapper
export function selectPowerOfTen(val: number): { divider: number, unit: string } {
export function selectPowerOfTen(val: number, multiplier = 1): { divider: number, unit: string } {
const powerOfTen = {
exa: Math.pow(10, 18),
peta: Math.pow(10, 15),
@@ -236,17 +236,17 @@ export function selectPowerOfTen(val: number): { divider: number, unit: string }
};
let selectedPowerOfTen: { divider: number, unit: string };
if (val < powerOfTen.kilo) {
if (val < powerOfTen.kilo * multiplier) {
selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling
} else if (val < powerOfTen.mega) {
} else if (val < powerOfTen.mega * multiplier) {
selectedPowerOfTen = { divider: powerOfTen.kilo, unit: 'k' };
} else if (val < powerOfTen.giga) {
} else if (val < powerOfTen.giga * multiplier) {
selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' };
} else if (val < powerOfTen.tera) {
} else if (val < powerOfTen.tera * multiplier) {
selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' };
} else if (val < powerOfTen.peta) {
} else if (val < powerOfTen.peta * multiplier) {
selectedPowerOfTen = { divider: powerOfTen.tera, unit: 'T' };
} else if (val < powerOfTen.exa) {
} else if (val < powerOfTen.exa * multiplier) {
selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' };
} else {
selectedPowerOfTen = { divider: powerOfTen.exa, unit: 'E' };

View File

@@ -0,0 +1,16 @@
<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" (click)="onSponsorClick($event)">Become a Community Sponsor</a>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Exclusive swag</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Your avatar on the About page</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> And more coming soon :)</p>
</div>
<div class="become-sponsor enterprise">
<p style="font-weight: 700; font-size: 18px;">If you're a business...</p>
<a href="https://mempool.space/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button" (click)="onEnterpriseClick($event)">Become an Enterprise Sponsor</a>
<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>

View File

@@ -0,0 +1,45 @@
#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,22 @@
import { Component } from '@angular/core';
import { EnterpriseService } from '../../services/enterprise.service';
@Component({
selector: 'app-about-sponsors',
templateUrl: './about-sponsors.component.html',
styleUrls: ['./about-sponsors.component.scss'],
})
export class AboutSponsorsComponent {
constructor(private enterpriseService: EnterpriseService) {
}
onSponsorClick(e): boolean {
this.enterpriseService.goal(5);
return true;
}
onEnterpriseClick(e): boolean {
this.enterpriseService.goal(6);
return true;
}
}

View File

@@ -32,12 +32,8 @@
<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>
</div>
<ng-container *ngIf="officialMempoolSpace">
<app-about-sponsors></app-about-sponsors>
</ng-container>
<div class="enterprise-sponsor" id="enterprise-sponsors">
@@ -186,14 +182,14 @@
</div>
<ng-container *ngIf="officialMempoolSpace">
<div *ngIf="profiles$ | async as profiles" id="community-sponsors">
<div *ngIf="profiles$ | async as profiles" id="community-sponsors-anchor">
<div class="community-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0">
<h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3>
<div class="wrapper">
<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 +201,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 +295,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

@@ -9,6 +9,7 @@ import { Router, ActivatedRoute } from '@angular/router';
import { map, share, tap } from 'rxjs/operators';
import { ITranslators } from '../../interfaces/node-api.interface';
import { DOCUMENT } from '@angular/common';
import { EnterpriseService } from '../../services/enterprise.service';
@Component({
selector: 'app-about',
@@ -33,6 +34,7 @@ export class AboutComponent implements OnInit {
private websocketService: WebsocketService,
private seoService: SeoService,
public stateService: StateService,
private enterpriseService: EnterpriseService,
private apiService: ApiService,
private router: Router,
private route: ActivatedRoute,
@@ -47,8 +49,13 @@ export class AboutComponent implements OnInit {
this.websocketService.want(['blocks']);
this.profiles$ = this.apiService.getAboutPageProfiles$().pipe(
tap(() => {
this.goToAnchor()
tap((profiles: any) => {
const scrollToSponsors = this.route.snapshot.fragment === 'community-sponsors';
if (scrollToSponsors && !profiles?.whales?.length && !profiles?.chads?.length) {
return;
} else {
this.goToAnchor(scrollToSponsors)
}
}),
share(),
)
@@ -83,11 +90,19 @@ export class AboutComponent implements OnInit {
this.goToAnchor();
}
goToAnchor() {
goToAnchor(scrollToSponsor = false) {
if (!scrollToSponsor) {
return;
}
setTimeout(() => {
if (this.route.snapshot.fragment) {
if (this.document.getElementById(this.route.snapshot.fragment)) {
this.document.getElementById(this.route.snapshot.fragment).scrollIntoView({behavior: 'smooth'});
const el = scrollToSponsor ? this.document.getElementById('community-sponsors-anchor') : this.document.getElementById(this.route.snapshot.fragment);
if (el) {
if (scrollToSponsor) {
el.scrollIntoView({behavior: 'smooth', block: 'center', inline: 'center'});
} else {
el.scrollIntoView({behavior: 'smooth'});
}
}
}
}, 1);
@@ -108,4 +123,14 @@ export class AboutComponent implements OnInit {
unmutePromoVideo(): void {
this.promoVideo.nativeElement.muted = false;
}
onSponsorClick(e): boolean {
this.enterpriseService.goal(5);
return true;
}
onEnterpriseClick(e): boolean {
this.enterpriseService.goal(6);
return true;
}
}

View File

@@ -0,0 +1,45 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { AboutComponent } from './about.component';
import { AboutSponsorsComponent } from './about-sponsors.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,
AboutSponsorsComponent,
],
exports: [
AboutSponsorsComponent,
]
})
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>
@@ -25,6 +27,11 @@
<ng-container *ngIf="estimate">
<div [class]="{estimateDisabled: error}">
<div *ngIf="user && !estimate.hasAccess">
<div class="alert alert-mempool">You are currently on the waitlist</div>
</div>
<h5>Your transaction</h5>
<div class="row">
<div class="col">
@@ -37,10 +44,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 +55,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 +81,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 +94,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 +115,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 +135,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 +147,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 +164,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 +187,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 +210,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>
@@ -251,7 +235,7 @@
<div class="row mb-3" *ngIf="isLoggedIn()">
<div class="col">
<div class="d-flex justify-content-end">
<div class="d-flex justify-content-end" *ngIf="user && estimate.hasAccess">
<button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()">Accelerate</button>
</div>
</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,15 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
maxCost = 0;
userBid = 0;
selectFeeRateIndex = 1;
showTable: 'estimated' | 'maximum' = 'maximum';
isMobile: boolean = window.innerWidth <= 767.98;
user: any = undefined;
maxRateOptions: RateOption[] = [];
constructor(
private apiService: ApiService,
private storageService: StorageService
private storageService: StorageService,
private cd: ChangeDetectorRef
) { }
ngOnDestroy(): void {
@@ -73,11 +74,13 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
ngOnChanges(changes: SimpleChanges): void {
if (changes.scrollEvent) {
this.scrollToPreview('acceleratePreviewAnchor', 'center');
this.scrollToPreview('acceleratePreviewAnchor', 'start');
}
}
ngOnInit() {
this.user = this.storageService.getAuth()?.user ?? null;
this.estimateSubscription = this.apiService.estimate$(this.tx.txid).pipe(
tap((response) => {
if (response.status === 204) {
@@ -93,7 +96,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
this.estimateSubscription.unsubscribe();
}
if (this.estimate.userBalance <= 0) {
if (this.estimate.hasAccess === true && this.estimate.userBalance <= 0) {
if (this.isLoggedIn()) {
this.error = `not_enough_balance`;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
@@ -126,7 +129,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 +165,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 +191,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

@@ -69,7 +69,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
this.addressString = this.addressString.toLowerCase();
}
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'} ${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`);
return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
? this.electrsApiService.getPubKeyAddress$(this.addressString)

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

@@ -55,7 +55,7 @@
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
<td *ngSwitchCase="'rbf'"><span class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span></td>
<td *ngSwitchCase="'accelerated'"><span class="badge badge-success" i18n="transaction.audit.accelerated">Accelerated</span></td>
<td *ngSwitchCase="'accelerated'"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></td>
</ng-container>
</tr>
</tbody>

View File

@@ -19,4 +19,17 @@
.td-width {
padding-right: 10px;
}
.badge.badge-accelerated {
background-color: #653b9c;
box-shadow: #ad7de57f 0px 0px 12px -2px;
color: white;
animation: acceleratePulse 1s infinite;
}
@keyframes acceleratePulse {
0% { background-color: #653b9c; box-shadow: #ad7de57f 0px 0px 12px -2px; }
50% { background-color: #8457bb; box-shadow: #ad7de5 0px 0px 18px -2px;}
100% { background-color: #653b9c; box-shadow: #ad7de57f 0px 0px 12px -2px; }
}

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

@@ -219,13 +219,13 @@
<div class="box" *ngIf="!error && webGlEnabled && showAudit">
<div class="nav nav-tabs" *ngIf="isMobile && showAudit">
<a class="nav-link" [class.active]="mode === 'projected'"
fragment="projected" (click)="changeMode('projected')"><ng-container i18n="block.expected">Expected</ng-container>&nbsp;&nbsp;<span class="badge badge-pill badge-warning" i18n="beta">beta</span></a>
fragment="projected" (click)="changeMode('projected')"><ng-container i18n="block.expected">Expected</ng-container></a>
<a class="nav-link" [class.active]="mode === 'actual'" i18n="block.actual"
fragment="actual" (click)="changeMode('actual')">Actual</a>
</div>
<div class="row">
<div class="col-sm">
<h3 class="block-subtitle" *ngIf="!isMobile"><ng-container i18n="block.expected-block">Expected Block</ng-container> <span class="badge badge-pill badge-warning beta" i18n="beta">beta</span></h3>
<h3 class="block-subtitle" *ngIf="!isMobile"><ng-container i18n="block.expected-block">Expected Block</ng-container></h3>
<div class="block-graph-wrapper">
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="86"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"
@@ -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' },
@@ -249,10 +249,8 @@ export class HashrateChartComponent implements OnInit {
for (const tick of ticks) {
if (tick.seriesIndex === 0) { // Hashrate
let hashrate = tick.data[1];
if (this.isMobile()) {
hashratePowerOfTen = selectPowerOfTen(tick.data[1]);
hashrate = Math.round(tick.data[1] / hashratePowerOfTen.divider);
}
hashratePowerOfTen = selectPowerOfTen(tick.data[1], 10);
hashrate = tick.data[1] / hashratePowerOfTen.divider;
hashrateString = `${tick.marker} ${tick.seriesName}: ${formatNumber(hashrate, this.locale, '1.0-0')} ${hashratePowerOfTen.unit}H/s<br>`;
} else if (tick.seriesIndex === 1) { // Difficulty
let difficultyPowerOfTen = hashratePowerOfTen;
@@ -260,18 +258,14 @@ export class HashrateChartComponent implements OnInit {
if (difficulty === null) {
difficultyString = `${tick.marker} ${tick.seriesName}: No data<br>`;
} else {
if (this.isMobile()) {
difficultyPowerOfTen = selectPowerOfTen(tick.data[1]);
difficulty = Math.round(tick.data[1] / difficultyPowerOfTen.divider);
}
difficultyPowerOfTen = selectPowerOfTen(tick.data[1]);
difficulty = tick.data[1] / difficultyPowerOfTen.divider;
difficultyString = `${tick.marker} ${tick.seriesName}: ${formatNumber(difficulty, this.locale, '1.2-2')} ${difficultyPowerOfTen.unit}<br>`;
}
} else if (tick.seriesIndex === 2) { // Hashrate MA
let hashrate = tick.data[1];
if (this.isMobile()) {
hashratePowerOfTen = selectPowerOfTen(tick.data[1]);
hashrate = Math.round(tick.data[1] / hashratePowerOfTen.divider);
}
hashratePowerOfTen = selectPowerOfTen(tick.data[1], 10);
hashrate = tick.data[1] / hashratePowerOfTen.divider;
hashrateStringMA = `${tick.marker} ${tick.seriesName}: ${formatNumber(hashrate, this.locale, '1.0-0')} ${hashratePowerOfTen.unit}H/s`;
}
}
@@ -342,7 +336,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 +358,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: {
@@ -210,8 +209,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80;
return obj;
},
extraCssText: `width: ${(['2h', '24h'].includes(this.windowPreference) || this.template === 'widget') ? '125px' : '135px'};
background: transparent;
extraCssText: `background: transparent;
border: none;
box-shadow: none;`,
axisPointer: {
@@ -234,7 +232,8 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
}
}
});
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}">${itemFormatted}</div>`;
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}"
style="width: ${(this.windowPreference === '2h' || this.template === 'widget') ? '125px' : '215px'}">${itemFormatted}</div>`;
}
},
xAxis: [
@@ -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();
}
}

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