Compare commits

...

255 Commits

Author SHA1 Message Date
wiz
876feef53f Fix frontend docker entrypoint umbrel LND detection 2023-03-21 18:11:10 +09:00
wiz
f72e17c12e Merge pull request #3491 from mempool/simon/audit-off-hide-health
Audit disabled related UX fixes
2023-03-21 18:02:24 +09:00
wiz
f570b2762f Merge branch 'master' into simon/audit-off-hide-health 2023-03-21 17:43:56 +09:00
wiz
e2fda99578 Merge pull request #3490 from mempool/simon/auto-disable-ln-on-macaroon-fail
Auto disable LN on macaroon fail
2023-03-21 17:43:12 +09:00
softsimon
45dbc6c6f6 Update logger network after modifying config 2023-03-21 16:21:11 +09:00
softsimon
d76e3a5939 Audit disabled related UX fixes 2023-03-21 16:02:46 +09:00
wiz
cb8fdb5e8d Hack docker frontend entrypoint to auto-enable lightning 2023-03-21 15:57:22 +09:00
softsimon
d337bf3ee2 Turn off LN if Macaroon is missing 2023-03-21 15:52:41 +09:00
softsimon
758e4d4f4c Disable LN on macaroon fail 2023-03-21 15:49:38 +09:00
softsimon
cd2bda4b49 Pull russian from transifex 2023-03-20 21:44:47 +09:00
wiz
493ea0641d Merge pull request #3487 from mempool/simon/catch-unhandled-lnd-axios-request
Catch exeptions in Lightning stats
2023-03-20 21:42:47 +09:00
wiz
ca1b6553c9 Merge branch 'master' into simon/catch-unhandled-lnd-axios-request 2023-03-20 20:53:41 +09:00
wiz
d479715d8e Merge pull request #3450 from mempool/nymkappa/configurable-timeout
Make core and lnd rpc calls timeout configurable
2023-03-20 20:50:51 +09:00
softsimon
e3109a8fec Catch exeptions in Lightning stats
fixes #3486
2023-03-20 20:46:11 +09:00
softsimon
e6bc5bef33 Updating russian i18n 2023-03-20 18:50:20 +09:00
softsimon
d82a7169b7 Pull from transifex 2023-03-20 18:28:15 +09:00
wiz
ba48b6f7ce Merge branch 'master' into nymkappa/configurable-timeout 2023-03-20 18:26:04 +09:00
wiz
8e1cf997f7 Merge pull request #3451 from mempool/simon/mining-difficulty-overflow-fix
Difficulty mining ellipsis fix
2023-03-20 18:25:51 +09:00
wiz
70d8548c92 Merge branch 'master' into simon/mining-difficulty-overflow-fix 2023-03-20 17:45:27 +09:00
wiz
cce7dd917f Merge branch 'master' into nymkappa/configurable-timeout 2023-03-20 17:38:40 +09:00
wiz
3dafb284a9 Merge pull request #3447 from mempool/hunicus/readme-video-poster
Add poster image for readme video
2023-03-20 17:37:21 +09:00
wiz
6a599a9a30 Merge pull request #3448 from mempool/simon/fix-missing-temp-cache-disk-cache
Fix missing temp cache in disk cache
2023-03-20 17:33:41 +09:00
softsimon
74fb292633 Difficulty mining ellipsis fix 2023-03-20 17:21:34 +09:00
nymkappa
c6e063ea2f Make lnd timeout configurable 2023-03-20 16:35:44 +09:00
nymkappa
81d563381a Make bitcoin core timeout configurable 2023-03-20 16:15:40 +09:00
softsimon
870e895144 Correcting docker disk cache config variable 2023-03-20 16:12:56 +09:00
softsimon
343d1345e2 Merge pull request #3445 from mempool/simon/liquid-blinding-tests-failing
Fixing broken liquid blinding tests
2023-03-20 15:46:41 +09:00
softsimon
517cf613c1 Removing Sigterm. Cache write block interval configuration. 2023-03-20 15:46:05 +09:00
softsimon
d54bcc898b Fix missing temp cache in disk cache 2023-03-20 15:44:55 +09:00
wiz
704e1741ed Merge pull request #3449 from mempool/nymkappa/fix-tests 2023-03-20 14:22:01 +09:00
nymkappa
ad5ce6dba4 Fix maxmind tests 2023-03-20 14:02:31 +09:00
hunicus
1ed20a95df Add poster image for readme video
Also add line-break after video and remove screenshot.
2023-03-19 22:58:39 -04:00
wiz
1718ddd4c3 Merge pull request #3444 from mempool/nymkappa/missing-docker
Update docker configs
2023-03-19 18:56:06 +09:00
wiz
32c2db2153 Fix backend docker config path for GeoIP data 2023-03-19 18:42:38 +09:00
wiz
0abb9cbb7c Fix boolean configuration option in docker backend config 2023-03-19 17:49:37 +09:00
wiz
e2e71c7a46 Add Maxmind GeoIP Lite download to Docker build 2023-03-19 17:49:08 +09:00
softsimon
e27bdd3e2b Fixing broken liquid blinding tests 2023-03-19 17:30:06 +09:00
softsimon
5839ed428e Merge pull request #3378 from mempool/nymkappa/responsive
Fix block list component responsive
2023-03-19 16:48:47 +09:00
nymkappa
af6d115dbb Add missing MAXMIND in docker configs - Remove duplicated __MEMPOOL_INDEXING_BLOCKS_AMOUNT__ 2023-03-19 15:39:17 +09:00
nymkappa
194968d16f Fix block list component responsive 2023-03-19 15:23:00 +09:00
softsimon
30686bd322 Updating korean translation 2023-03-19 12:32:07 +09:00
softsimon
175c645777 Crediting korean translator 2023-03-19 12:05:13 +09:00
wiz
587a259843 Merge pull request #3440 from mempool/ops/bump-elements-v22.1
ops: Bump elementsd to v22.1
2023-03-18 20:06:53 +09:00
softsimon
64749ca726 Pull from transifex 2023-03-18 19:39:07 +09:00
wiz
8f2493dadb ops: Use old elements RPC port 7040 2023-03-18 18:48:37 +09:00
softsimon
7d8ea075d9 Merge pull request #3405 from mempool/nymkappa/pool-health
Show block health in pool block list
2023-03-18 18:48:10 +09:00
wiz
328327e5dc Merge pull request #3104 from mempool/mononaut/liquid-tooltip-fees
Fix missing fees in liquid block tooltips
2023-03-18 18:33:17 +09:00
wiz
fd1816d451 Merge pull request #3402 from mempool/hunicus/promo-subtitles
Add subtitles for promo video
2023-03-18 18:27:35 +09:00
hunicus
72f25b873c Add zh subtitles for promo video 2023-03-18 05:17:23 -04:00
hunicus
994656953c Fix promo video subtitles for sv 2023-03-18 05:15:48 -04:00
Mononaut
ed46232b83 Fix missing fees in liquid block tooltips 2023-03-18 18:11:10 +09:00
wiz
dca18a1c66 Merge branch 'master' into hunicus/promo-subtitles 2023-03-18 17:59:03 +09:00
wiz
c291ee1789 Merge pull request #3407 from mempool/hunicus/mempool-size-faq
Add faqs on mempool size and memory usage
2023-03-18 17:58:56 +09:00
hunicus
caf15351f7 Merge branch 'master' into hunicus/promo-subtitles 2023-03-18 17:58:29 +09:00
hunicus
6be9f23790 Add more languages for promo captions 2023-03-18 04:57:20 -04:00
nymkappa
adc51f6217 Update i18n 2023-03-18 16:40:30 +09:00
softsimon
ec8a46ede6 Pulling from transifex 2023-03-18 15:33:57 +09:00
softsimon
b78fdf5a23 Merge pull request #3353 from mempool/mononaut/mempool-block-animations
Improve mempool block animations
2023-03-18 12:46:20 +09:00
wiz
7c2493f3fa ops: Bump elementsd to v22.1 2023-03-17 22:07:48 +09:00
hunicus
b0a0ad11b4 Show mempool.space memory usage faq on official 2023-03-17 21:25:28 +09:00
hunicus
377f71eb52 Integrate feedback to memory usage faqs 2023-03-17 21:25:28 +09:00
hunicus
eefe343973 Add faqs on mempool size and memory usage 2023-03-17 21:25:28 +09:00
softsimon
41a6674fad Merge pull request #3379 from mempool/nymkappa/testnet-signet-price-zero
Don't fetch prices on signet/testnet, always show 0
2023-03-17 16:40:09 +09:00
softsimon
effba92729 Merge pull request #3381 from mempool/mononaut/network-special-blocks
Limit special blocks by network, add future halvings
2023-03-17 16:39:54 +09:00
wiz
c9f4bdda17 Merge pull request #3408 from knorrium/gha_docker_digest
Docker digest GHA
2023-03-17 14:49:19 +09:00
softsimon
ef54385068 Merge pull request #3413 from mempool/simon/pull-from-transifex-2023-03-17
Pull from transifex
2023-03-17 14:02:08 +09:00
softsimon
b8f77c4be4 Pull from transifex 2023-03-17 14:01:36 +09:00
Mononaut
b5c2073414 Fix getSimilarity error on empty mempool 2023-03-16 22:16:40 +09:00
Mononaut
25aacb5046 Calculate similarity score with audit disabled 2023-03-16 22:16:40 +09:00
Mononaut
c24724dcdf animate mempool blocks conditional on mined block similarity 2023-03-16 22:16:40 +09:00
Mononaut
64ab14f995 mempool block entry animation 2023-03-16 22:16:40 +09:00
nymkappa
4a64c0dfa5 Fix skeleton 2023-03-16 16:35:59 +09:00
nymkappa
0ebe0a5dc9 Add new stats in mining pool page 2023-03-16 16:13:11 +09:00
hunicus
c683a52a01 Show captions for non-english locales 2023-03-16 02:29:33 -04:00
Felipe Knorr Kuhn
5fbdd0bb2a Remove push trigger 2023-03-15 22:48:28 -07:00
Felipe Knorr Kuhn
599881366b Add GHA for Docker digest 2023-03-15 22:43:41 -07:00
hunicus
4c294b010d Make subtitles default to current locale 2023-03-15 23:06:10 -04:00
hunicus
418c32e334 Make git ignore mp4 and vtt files in /resources 2023-03-15 23:06:10 -04:00
hunicus
12b605e5cc Fetch subtitles files from github
For en, sv, and ja.
2023-03-15 23:06:10 -04:00
softsimon
870a7e51b1 Merge pull request #3373 from mempool/mononaut/fix-testnet-signet-features
Network-specific activation heights for transaction feature badges
2023-03-15 18:35:40 +09:00
hunicus
cdfde05452 Make new folder for promo video assets
Video, cover image, and subtitle files.
2023-03-15 02:50:17 -04:00
softsimon
ce24b8bb0a Merge pull request #3331 from mempool/mononaut/disk-cache-network-version
Add network versioning to disk cache
2023-03-15 14:32:28 +09:00
softsimon
1b2810ec0e Merge branch 'master' into mononaut/disk-cache-network-version 2023-03-14 21:05:16 +09:00
softsimon
c8e84ec056 Merge pull request #3329 from mempool/mutiny-integration
Add mutiny as community integration
2023-03-14 19:59:43 +09:00
Mononaut
7bf8fea9f2 Limit special blocks by network, add future halvings 2023-03-14 16:58:02 +09:00
softsimon
3458c5af71 Merge branch 'master' into mononaut/fix-testnet-signet-features 2023-03-14 16:50:06 +09:00
softsimon
3e46dabf7b Merge pull request #3345 from mempool/mononaut/hide-empty-features
Hide features row if tx has no features
2023-03-14 16:48:56 +09:00
nymkappa
a5dd141934 Don't fetch prices on signet/testnet, always show 0 2023-03-14 15:39:15 +09:00
softsimon
881af309ab Pull from transifex 2023-03-14 14:54:49 +09:00
softsimon
374ff50a62 Merge pull request #3376 from mempool/i18n/enable-danish
i18n: Enable Danish
2023-03-14 14:53:00 +09:00
wiz
2b9e63dbc5 i18n: Enable Danish 2023-03-14 14:31:20 +09:00
Mononaut
9f453deceb Use network-specific feature activation dates 2023-03-14 13:02:50 +09:00
softsimon
0b88d94573 Fix channels i18n string on world map
fixes #3336
2023-03-13 18:42:43 +09:00
wiz
536114853c Merge pull request #3351 from mempool/nymkappa/disable-pool-updater-no-mempool
Disable pool update when running lightning only
2023-03-13 18:16:29 +09:00
wiz
4d281277d6 Merge branch 'master' into nymkappa/disable-pool-updater-no-mempool 2023-03-13 18:02:03 +09:00
wiz
c97a722a3b Merge pull request #3350 from mempool/simon/video-height-fix
Correct video height on mobile
2023-03-13 18:01:17 +09:00
softsimon
adacb42d1a Merge branch 'master' into simon/video-height-fix 2023-03-13 17:41:45 +09:00
softsimon
b6427d6f67 Merge branch 'master' into nymkappa/disable-pool-updater-no-mempool 2023-03-13 17:41:04 +09:00
softsimon
433acb7b1d Merge pull request #3352 from mempool/simon/i18n-string-error-fix
Fixes i18n string error
2023-03-13 17:40:52 +09:00
softsimon
d015ee7824 Fixes i18n string error 2023-03-13 17:40:17 +09:00
nymkappa
ecfb980e75 Disable pool update when running lightning only 2023-03-13 17:24:23 +09:00
softsimon
cfdbd30956 Correct video height on mobile
#3342
2023-03-13 17:18:09 +09:00
softsimon
7acfec2406 Merge pull request #3349 from mempool/simon/update-missing-i18n-strings
Fixing more missing i18n keys
2023-03-13 17:08:50 +09:00
softsimon
070ee10fb0 Fixing more missing i18n keys
fixes #3339
fixes #3337
2023-03-13 17:08:27 +09:00
softsimon
446b0de8f3 Merge pull request #3347 from mempool/simon/ninja-i18n-pull
Ninja i18n pull
2023-03-13 13:04:13 +09:00
softsimon
99b7fc8814 Ninja i18n pull 2023-03-13 13:03:57 +09:00
softsimon
defb88a474 Merge pull request #3346 from mempool/simon/i18n-extract-2023-03-13
i18n extract
2023-03-13 12:58:48 +09:00
softsimon
7392535182 i18n extract 2023-03-13 12:58:35 +09:00
Mononaut
130ae8c3a5 Hide features row if tx has no features 2023-03-13 12:48:01 +09:00
softsimon
f4f8b2b271 Merge pull request #3344 from mempool/simon/pull-from-transifex-2023-03-13
Pull from transifex
2023-03-13 12:36:13 +09:00
softsimon
477d09412b Pull from transifex 2023-03-13 12:36:00 +09:00
softsimon
87bc7917d8 Merge pull request #3341 from mempool/mononaut/difficulty-skeleton-width
Fix difficulty skeleton width on firefox
2023-03-13 11:23:17 +09:00
Mononaut
9030d95207 Fix difficulty skeleton width on firefox 2023-03-13 10:28:26 +09:00
softsimon
5e4131b474 Merge pull request #3330 from mempool/mononaut/reduce-disk-cache-frequency
Save cache to disk every 6 blocks
2023-03-12 21:06:19 +09:00
Mononaut
3bf96dafde Add network versioning to disk cache 2023-03-12 19:20:29 +09:00
softsimon
2f4dba895c Merge pull request #3327 from mempool/nymkappa/bugfix/show-total-fee-last-mempool-block
Show cumulated fee on last mempool block
2023-03-12 19:17:57 +09:00
hunicus
a5e281706f Make community integration rows symmetric and full 2023-03-12 06:17:50 -04:00
nymkappa
eba0e1c25a Show cumulated fee on last mempool block 2023-03-12 19:13:39 +09:00
hunicus
97d82042c0 Add mutiny to community integrations 2023-03-12 06:03:23 -04:00
Mononaut
8bd05987e5 Save cache to disk every 6 blocks 2023-03-12 19:03:19 +09:00
wiz
7e676dbaf0 Merge pull request #3325 from mempool/mononaut/fix-liquid-asset-tooltips
Fix units in flow diagram tooltips for liquid assets
2023-03-12 18:31:49 +09:00
softsimon
760f3193d9 Merge pull request #3324 from mempool/hunicus/add-integrations-032023
Add 4 community integrations to about page
2023-03-12 18:15:02 +09:00
wiz
72663b30df Merge branch 'master' into mononaut/fix-liquid-asset-tooltips 2023-03-12 18:14:25 +09:00
hunicus
8995283a58 Remove trailing slash 2023-03-12 05:12:25 -04:00
Mononaut
a8ac6aedf7 Fix units in flow diagram tooltips for liquid assets 2023-03-12 18:07:31 +09:00
wiz
9613247283 Merge branch 'master' into hunicus/add-integrations-032023 2023-03-12 18:03:49 +09:00
hunicus
ab96a17e80 Make non-rounded icons smaller 2023-03-12 04:59:37 -04:00
hunicus
3b080ee5fb Add galoy and boltz to community integrations 2023-03-12 04:52:15 -04:00
hunicus
24a8cca758 Add bitcoin-s and edge to community integrations 2023-03-12 04:51:59 -04:00
softsimon
dbad3af8ba Merge pull request #3322 from mempool/mononaut/fix-new-block-animations
Fix new block animations
2023-03-12 17:42:33 +09:00
softsimon
cdddf3a8b2 Merge pull request #3319 from mempool/mononaut/more-fee-ratings
Show tx fee ratings for older blocks
2023-03-12 17:10:47 +09:00
Mononaut
b675bd8d55 Fix transaction confirmed arrow animation 2023-03-12 17:00:36 +09:00
wiz
cb04d67d07 Merge branch 'master' into mononaut/more-fee-ratings 2023-03-12 16:56:18 +09:00
wiz
b1c6c5cbb6 Merge pull request #3248 from mempool/hunicus/add-yt-pt
Add youtube and peertube links to about page
2023-03-12 16:54:59 +09:00
wiz
7f8f72e9d6 Merge pull request #3306 from mempool/simon/fix-use-same-fee-span-calc
Display same fee span on blocks
2023-03-12 16:52:55 +09:00
wiz
a1ea77ee50 Merge pull request #3311 from mempool/simon/lightning-stats-truncation
Lightning dashboard overflow titles fixes
2023-03-12 16:51:38 +09:00
wiz
19e2d687d0 Use correct BitcoinTV logo 2023-03-12 16:48:23 +09:00
Mononaut
4be8016eb1 Fix repeated new block animation on page navigation 2023-03-12 16:42:58 +09:00
wiz
627c913d5e Merge branch 'master' into simon/fix-use-same-fee-span-calc 2023-03-12 16:17:51 +09:00
wiz
3c5165603b Merge branch 'master' into simon/lightning-stats-truncation 2023-03-12 16:17:22 +09:00
hunicus
513277cef7 Replace peertube logo with bitcointv logo 2023-03-12 16:09:06 +09:00
hunicus
72a9b71901 Add youtube and peertube links to about page 2023-03-12 16:09:06 +09:00
Mononaut
60bef0eeb6 show tx fee ratings for older blocks 2023-03-12 15:54:39 +09:00
softsimon
eeb97bdb81 Merge pull request #3318 from mempool/simon/pull-transifex-2023-03-12
Pull from transifex
2023-03-12 15:45:44 +09:00
softsimon
8407c07b88 Pull from transifex 2023-03-12 15:44:49 +09:00
softsimon
951cf2daf9 Merge pull request #3314 from mempool/nymkappa/fix-node-distance-overflow
Fix some responsive issue on the node component
2023-03-12 14:37:27 +09:00
nymkappa
31cfbf6625 Fix some responsive issue on the node component 2023-03-12 10:16:49 +09:00
wiz
5dc52250e6 Merge pull request #3312 from mempool/mononaut/difficulty-widget-redesign
Fix epoch length in new difficulty widget
2023-03-11 19:49:07 +09:00
wiz
6b544ac179 Merge branch 'master' into mononaut/difficulty-widget-redesign 2023-03-11 19:38:15 +09:00
wiz
2ad9bf57f7 Merge pull request #3288 from mempool/nymkappa/esplora-warning
Log a warn if there are lot of 404 from esplora tx api while updating nodejs backend mempool
2023-03-11 19:36:51 +09:00
Mononaut
e0e97f0d5e Fix epoch length in difficulty widget 2023-03-11 19:32:59 +09:00
softsimon
c72024b4e3 Lightning dashboard overflow titles fixes
fixes #3127
2023-03-11 19:31:49 +09:00
wiz
093469c164 Merge pull request #3303 from mempool/mononaut/batch-address-outspend-lookup
batch address outspend lookups into <50 txids per request
2023-03-11 19:27:50 +09:00
wiz
24d9977919 Merge branch 'master' into nymkappa/esplora-warning 2023-03-11 18:36:22 +09:00
softsimon
7b2ea9c4c8 Merge pull request #3310 from mempool/simon/next-block-lower-case-css-fix
next block lower case css fix
2023-03-11 18:35:08 +09:00
softsimon
6b4650f3cd next block lower case css fix 2023-03-11 18:34:51 +09:00
softsimon
e971846b7e Merge pull request #3309 from mempool/simon/fix-i18n-duplicate-warning
Fixes i18n duplicate warning
2023-03-11 18:33:08 +09:00
softsimon
23ea5d582b Fixes i18n duplicate warning 2023-03-11 18:32:47 +09:00
softsimon
6196860387 Merge pull request #3296 from mempool/nymkappa/order-isp
Sort asn numerically - add few more top 10 isp in warm cache
2023-03-11 18:22:39 +09:00
softsimon
368f858ff2 Merge pull request #3258 from mempool/mononaut/difficulty-widget-redesign
Redesign difficulty adjustment dashboard widget
2023-03-11 18:11:34 +09:00
softsimon
ef0cc9d2db Changing interval to block time 2023-03-11 18:04:06 +09:00
Mononaut
39051e94e3 Redesign difficulty adjustment dashboard widget 2023-03-11 17:53:18 +09:00
softsimon
e1f0bb9901 Merge pull request #3308 from mempool/simon/pull-from-transifex-2023-03-11
Pull from transifex 2023-03-11
2023-03-11 17:26:50 +09:00
softsimon
097eed0baa Pull from transifex 2023-03-11 2023-03-11 17:26:33 +09:00
softsimon
25c7c84705 Merge pull request #3297 from mempool/nymkappa/duplicate-block
Fixes duplicate block in latest block component
2023-03-11 16:09:40 +09:00
wiz
7e873e6637 Merge branch 'master' into nymkappa/order-isp 2023-03-11 15:50:31 +09:00
wiz
36e1777b96 Merge branch 'master' into mononaut/batch-address-outspend-lookup 2023-03-11 15:16:59 +09:00
wiz
35a05a420d Merge branch 'master' into nymkappa/duplicate-block 2023-03-11 15:15:56 +09:00
softsimon
1be5c6ec53 Merge pull request #3307 from mempool/nymkappa/update-regtest-examples
Updated regtest example
2023-03-11 11:37:15 +09:00
softsimon
e4aa3c2091 Merge pull request #3304 from mempool/simon/fix-confirmation-arrow-bug
Fixes arrow position on confirmed blocks
2023-03-11 11:31:30 +09:00
nymkappa
4263977d99 Updated regtest example 2023-03-11 10:52:15 +09:00
softsimon
5bd9be7ab1 Display same fee span on blocks
fixes #3282
2023-03-11 10:14:35 +09:00
softsimon
74c95bfdf5 Merge pull request #3300 from mempool/simon/fix-transaction-expression-changed-error
Fixes changed after checked error in transaction page
2023-03-11 10:01:58 +09:00
softsimon
a1c0a58b9d Merge pull request #3305 from mempool/simon/remove-search-bar-border
Remove search bar focus border
2023-03-11 09:56:27 +09:00
softsimon
d3d67627f3 Remove search bar focus border 2023-03-10 20:19:37 +09:00
softsimon
4a0d9cb66f Fixes arrow position on confirmed blocks
fixes #3294
2023-03-10 18:35:26 +09:00
Mononaut
a2d673f9ed batch address outspend lookups into <50 txids per request 2023-03-10 00:27:22 -06:00
softsimon
8dccaee0b0 Merge pull request #3299 from mempool/mononaut/persist-cache-on-exit
Save cache to disk on SIGTERM/SIGINT
2023-03-10 14:27:00 +09:00
softsimon
ccc413a800 Merge pull request #3301 from mempool/mononaut/flow-diagram-alignment
pixel-perfect flow diagrams (again)
2023-03-10 13:38:56 +09:00
softsimon
4d7e23064c Merge pull request #3298 from mempool/mononaut/missing-fee-rating
fix missing cpfp fee ratings on mobile
2023-03-10 13:28:14 +09:00
Mononaut
a22a62836e pixel-perfect flow diagrams 2023-03-09 22:20:54 -06:00
softsimon
dd01371b61 Fixes changed after checked error in transaction page 2023-03-10 12:37:55 +09:00
Mononaut
46d89ac837 prevent disk cache file write corruption 2023-03-09 20:19:22 -06:00
Mononaut
796566e7ae Save cache to disk on SIGTERM/SIGINT 2023-03-09 19:47:54 -06:00
Mononaut
3f234431fb fix missing fee rating on mobile 2023-03-09 19:31:53 -06:00
nymkappa
778e2f9b64 Fixes #3217 2023-03-10 10:26:30 +09:00
nymkappa
6327ce7c89 Sort asn numerically - add few more top 10 isp in warm cache 2023-03-10 09:21:44 +09:00
softsimon
cec8445223 Merge pull request #3293 from mempool/nymkappa/search-autofocus
Autofocus search input when we load the app for the first time
2023-03-09 19:04:03 +09:00
nymkappa
548a6ea664 Autofocus search input when we load the app for the first time 2023-03-09 18:53:29 +09:00
softsimon
9cef9b67c1 Merge pull request #3291 from mempool/revert-3290-nymkappa/bump-axios
Revert "Bump axios from 0.27.2 -> 1.3.4"
2023-03-09 17:46:19 +09:00
softsimon
42228dc70f Revert "Bump axios from 0.27.2 -> 1.3.4" 2023-03-09 17:46:09 +09:00
nymkappa
63dd9fd09e Log a warn if there are lot of 404 from esplora tx api 2023-03-09 17:45:08 +09:00
softsimon
cd3c1ed82e Merge pull request #3290 from mempool/nymkappa/bump-axios
Bump axios from 0.27.2 -> 1.3.4
2023-03-09 17:35:35 +09:00
nymkappa
304089b3d0 Bump axios from 0.27.2 -> 1.3.4 2023-03-09 17:27:19 +09:00
softsimon
c60903565e Merge pull request #3286 from mempool/simon/some-missing-ln-i18n-strings
Fixing Channels and Capacity i18n strings
2023-03-09 15:30:23 +09:00
softsimon
37e94249df Fixing Channels and Capacity i18n strings 2023-03-09 15:30:04 +09:00
softsimon
d2337ae4e8 Merge pull request #3285 from mempool/wiz/add-old-special-block-events
Add old special block events to app constants
2023-03-09 15:14:11 +09:00
softsimon
7557c47502 Merge pull request #3264 from mempool/mononaut/fix-mining-dashboard-updates
Fix stale mining dashboard data
2023-03-09 14:42:28 +09:00
wiz
a8214bcbbd Add old special block events to app constants 2023-03-09 14:30:55 +09:00
softsimon
fcf51e2af8 Merge pull request #3284 from mempool/simon/pull-transifex-2023-03-09
Pull from transifex
2023-03-09 13:19:40 +09:00
softsimon
c657e622eb Merge pull request #3277 from mempool/i18n/enable-danish-disable-catalan
i18n: Enable Danish, disable Catalan
2023-03-09 13:08:02 +09:00
softsimon
526e46b8e4 Pull from transifex 2023-03-09 11:49:26 +09:00
wiz
ead7a13ff0 i18n: Enable Danish, disable Catalan 2023-03-08 21:14:46 +09:00
Mononaut
c3c0696844 Update hashrate estimate when new blocks arrive 2023-03-08 02:37:19 -06:00
Mononaut
2907054a01 Update pool ranking block count when new blocks arrive 2023-03-08 02:33:16 -06:00
wiz
64408bfd16 Merge pull request #3241 from mempool/simon/fiat-space-fix
Remove fiat plus space
2023-03-08 17:02:19 +09:00
wiz
ca690bf123 Merge branch 'master' into simon/fiat-space-fix 2023-03-08 16:47:36 +09:00
softsimon
0243428fe9 Merge pull request #3262 from mempool/simon/pull-from-transifex-2023-03-08
Pull from transifex 8/3
2023-03-08 16:27:12 +09:00
softsimon
901d32d8f7 Pull from transifex 8/3 2023-03-08 16:26:56 +09:00
wiz
8adacd4a0e ops: Add missing unfurl route in nginx/server-common.conf 2023-03-08 16:14:43 +09:00
wiz
b8a3c15ed2 Merge pull request #3259 from mempool/mononaut/fix-cpfp-memory-bug
Fix memory-intensive getCPFPUnindexedBlocks mysql query
2023-03-08 15:10:31 +09:00
wiz
3e31e68a19 Merge branch 'master' into mononaut/fix-cpfp-memory-bug 2023-03-08 14:31:46 +09:00
wiz
626b395ab7 Merge pull request #3227 from mempool/simon/add-4y
Adding 4 year button to mempool graph
2023-03-08 14:31:39 +09:00
Mononaut
5eae84bb75 Fix memory-intensive getCPFPUnindexedBlocks mysql query 2023-03-07 21:01:54 -06:00
softsimon
c5a01135b3 Merge pull request #3249 from knorrium/fix_typo_in_bulk_config
Fix typo in bulk config variable
2023-03-08 11:05:58 +09:00
Felipe Knorr Kuhn
1a4f3b105e Fix typo in bulk config variable 2023-03-07 12:29:00 -08:00
wiz
ae1e2dcb50 Merge branch 'master' into simon/add-4y 2023-03-07 19:00:27 +09:00
softsimon
da25577ea7 Merge pull request #3244 from mempool/mononaut/fix-ln-rtl
Fix miscellaneous RTL layout bugs
2023-03-07 17:00:11 +09:00
Mononaut
5937e959c3 Fix miscellaneous RTL layout bugs 2023-03-06 20:25:27 -06:00
softsimon
6360913e84 Merge pull request #3243 from mempool/simon/pull-from-transifex-2023-03-07
Pull from transifex
2023-03-07 11:11:21 +09:00
softsimon
fb71136dae Pull from transifex 2023-03-07 11:11:02 +09:00
softsimon
3739e5be0d Merge pull request #3238 from mempool/mononaut/fix-404s
Fix unnecessary cpfp/rbf 404 responses
2023-03-06 18:40:32 +09:00
softsimon
ebfd0b9ddd Merge pull request #3225 from mempool/hunicus/responsive-disclaimer
Make faq disclaimer responsive
2023-03-06 16:50:03 +09:00
softsimon
355acfd338 Merge pull request #3239 from mempool/mononaut/fix-cached-rbf-error
don't cache tx data for rbf replacements
2023-03-06 16:45:36 +09:00
softsimon
5a5ebe8435 Remove fiat plus space
fixes #3240
2023-03-06 16:16:52 +09:00
Mononaut
43b2fe2f9a don't cache tx data for rbf replacements 2023-03-06 00:19:12 -06:00
Mononaut
182cb16695 Fix unnecessary cpfp 404 responses 2023-03-06 00:02:21 -06:00
softsimon
5a09e3099c Merge pull request #3237 from mempool/mononaut/unify-time-components
unify time rendering components
2023-03-06 12:24:00 +09:00
Mononaut
7f78fefb21 revert time localization strings 2023-03-05 21:09:22 -06:00
Mononaut
ac932c641c unify time rendering components 2023-03-05 19:26:32 -06:00
softsimon
4f297f0a7a Merge pull request #3234 from mempool/nymkappa/bugfix/ln-world-map-full-height
Show ln channel world map using 100% height
2023-03-05 19:20:28 +09:00
nymkappa
c28d1c4610 Show ln channel world map using 100% height 2023-03-05 17:43:59 +09:00
softsimon
1ad4ff0683 Merge pull request #3235 from mempool/simon/extract-i18n-clearnet
Update clearnet i18n string
2023-03-05 17:37:32 +09:00
softsimon
c50f5d45e1 Update clearnet i18n string 2023-03-05 17:37:15 +09:00
wiz
226a4e9bde Fix two more strings for "Clearnet Only" 2023-03-05 17:34:30 +09:00
wiz
154e65d470 Fix string for "Clearnet Only" 2023-03-05 17:30:32 +09:00
softsimon
a895d21179 Merge pull request #3231 from mempool/simon/i18n-fixes-extract
I18n extract. Some minor fixes.
2023-03-05 17:08:52 +09:00
softsimon
d5ea2aec25 I18n extract. Some minor fixes. 2023-03-05 17:08:35 +09:00
wiz
07271f56d7 Merge pull request #3230 from mempool/mononaut/heap-monitor
Monitor heap memory usage
2023-03-05 15:47:06 +09:00
wiz
f37946118c Change heap size warning to 80% utilization 2023-03-05 15:45:28 +09:00
softsimon
fdbcef29e5 Merge pull request #3212 from mempool/nymkappa/bugfix/initial-pool-download
Fix initial pool update when db is empty
2023-03-05 15:44:14 +09:00
wiz
9eeaf76369 Merge branch 'master' into mononaut/heap-monitor 2023-03-05 15:35:00 +09:00
wiz
a9a2ff0347 Merge pull request #3215 from mempool/nymkappa/bugfix/price
Handle missing price (show 0)
2023-03-05 15:34:31 +09:00
softsimon
aae61bcb45 Merge pull request #3125 from mempool/nymkappa/feature/update-mining-indexer-log
Update some mining indexer log
2023-03-05 15:19:50 +09:00
Mononaut
43bed7cf56 Monitor heap memory usage 2023-03-04 23:13:55 -06:00
wiz
fca813147d Merge branch 'master' into nymkappa/bugfix/price 2023-03-05 14:13:51 +09:00
nymkappa
62ef1d4439 Fix log typo 2023-03-05 08:27:31 +09:00
nymkappa
ff7c85180d Fix initial pool update when db is empty 2023-03-05 08:27:30 +09:00
nymkappa
001be82f5a Move some notice into info 2023-03-05 08:23:05 +09:00
nymkappa
32a260473a Update some mining indexing logs 2023-03-05 08:23:04 +09:00
nymkappa
2e74d7fa4a Remove mining db stats - replaced by runtime state variable 2023-03-05 08:23:04 +09:00
softsimon
4e39c27c75 Adding 4 year button to mempool graph
fixes #3218
2023-03-04 18:48:16 +09:00
hunicus
7b24b124c2 Use svg component for warning svg 2023-03-04 17:40:39 +09:00
hunicus
82b0844928 Make faq disclaimer more responsive 2023-03-04 17:40:39 +09:00
nymkappa
d483362a9b Handle missing price (show 0) 2023-03-04 10:51:13 +09:00
211 changed files with 26608 additions and 15623 deletions

26
.github/workflows/get_image_digest.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: 'Print images digest'
on:
workflow_dispatch:
inputs:
version:
description: 'Image Version'
required: false
default: 'latest'
type: string
jobs:
print-images-sha:
runs-on: 'ubuntu-latest'
name: Print digest for images
steps:
- name: Checkout
uses: actions/checkout@v3
with:
path: digest
- name: Run script
working-directory: digest
run: |
sh ./docker/scripts/get_image_digest.sh $VERSION
env:
VERSION: ${{ github.event.inputs.version }}

View File

@@ -1,13 +1,13 @@
# The Mempool Open Source Project™ [![mempool](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/ry4br7/master&style=flat-square)](https://dashboard.cypress.io/projects/ry4br7/runs) # The Mempool Open Source Project™ [![mempool](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/ry4br7/master&style=flat-square)](https://dashboard.cypress.io/projects/ry4br7/runs)
https://user-images.githubusercontent.com/232186/222445818-234aa6c9-c233-4c52-b3f0-e32b8232893b.mp4 https://user-images.githubusercontent.com/93150691/226236121-375ea64f-b4a1-4cc0-8fad-a6fb33226840.mp4
<br>
Mempool is the fully-featured mempool visualizer, explorer, and API service running at [mempool.space](https://mempool.space/). Mempool is the fully-featured mempool visualizer, explorer, and API service running at [mempool.space](https://mempool.space/).
It is an open-source project developed and operated for the benefit of the Bitcoin community, with a focus on the emerging transaction fee market that is evolving Bitcoin into a multi-layer ecosystem. It is an open-source project developed and operated for the benefit of the Bitcoin community, with a focus on the emerging transaction fee market that is evolving Bitcoin into a multi-layer ecosystem.
![mempool](https://mempool.space/resources/screenshots/v2.4.0-dashboard.png)
# Installation Methods # Installation Methods
Mempool can be self-hosted on a wide variety of your own hardware, ranging from a simple one-click installation on a Raspberry Pi full-node distro all the way to a robust production instance on a powerful FreeBSD server. Mempool can be self-hosted on a wide variety of your own hardware, ranging from a simple one-click installation on a Raspberry Pi full-node distro all the way to a robust production instance on a powerful FreeBSD server.

View File

@@ -171,52 +171,58 @@ Helpful link: https://gist.github.com/System-Glitch/cb4e87bf1ae3fec9925725bb3ebe
Run bitcoind on regtest: Run bitcoind on regtest:
``` ```
bitcoind -regtest -rpcport=8332 bitcoind -regtest
``` ```
Create a new wallet, if needed: Create a new wallet, if needed:
``` ```
bitcoin-cli -regtest -rpcport=8332 createwallet test bitcoin-cli -regtest createwallet test
``` ```
Load wallet (this command may take a while if you have lot of UTXOs): Load wallet (this command may take a while if you have lot of UTXOs):
``` ```
bitcoin-cli -regtest -rpcport=8332 loadwallet test bitcoin-cli -regtest loadwallet test
``` ```
Get a new address: Get a new address:
``` ```
address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress) address=$(bitcoin-cli -regtest getnewaddress)
``` ```
Mine blocks to the previously generated address. You need at least 101 blocks before you can spend. This will take some time to execute (~1 min): Mine blocks to the previously generated address. You need at least 101 blocks before you can spend. This will take some time to execute (~1 min):
``` ```
bitcoin-cli -regtest -rpcport=8332 generatetoaddress 101 $address bitcoin-cli -regtest generatetoaddress 101 $address
``` ```
Send 0.1 BTC at 5 sat/vB to another address: Send 0.1 BTC at 5 sat/vB to another address:
``` ```
./src/bitcoin-cli -named -regtest -rpcport=8332 sendtoaddress address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress) amount=0.1 fee_rate=5 bitcoin-cli -named -regtest sendtoaddress address=$(bitcoin-cli -regtest getnewaddress) amount=0.1 fee_rate=5
``` ```
See more example of `sendtoaddress`: See more example of `sendtoaddress`:
``` ```
./src/bitcoin-cli sendtoaddress # will print the help bitcoin-cli sendtoaddress # will print the help
``` ```
Mini script to generate transactions with random TX fee-rate (between 1 to 100 sat/vB). It's slow so don't expect to use this to test mempool spam, except if you let it run for a long time, or maybe with multiple regtest nodes connected to each other. Mini script to generate random network activity (random TX count with random tx fee-rate). It's slow so don't expect to use this to test mempool spam, except if you let it run for a long time, or maybe with multiple regtest nodes connected to each other.
``` ```
#!/bin/bash #!/bin/bash
address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress) address=$(bitcoin-cli -regtest getnewaddress)
bitcoin-cli -regtest generatetoaddress 101 $address
for i in {1..1000000} for i in {1..1000000}
do do
./src/bitcoin-cli -regtest -rpcport=8332 -named sendtoaddress address=$address amount=0.01 fee_rate=$(jot -r 1 1 100) for y in $(seq 1 "$(jot -r 1 1 1000)")
do
bitcoin-cli -regtest -named sendtoaddress address=$address amount=0.01 fee_rate=$(jot -r 1 1 100)
done
bitcoin-cli -regtest generatetoaddress 1 $address
sleep 5
done done
``` ```
Generate block at regular interval (every 10 seconds in this example): Generate block at regular interval (every 10 seconds in this example):
``` ```
watch -n 10 "./src/bitcoin-cli -regtest -rpcport=8332 generatetoaddress 1 $address" watch -n 10 "bitcoin-cli -regtest generatetoaddress 1 $address"
``` ```
### Mining pools update ### Mining pools update

View File

@@ -27,13 +27,15 @@
"AUDIT": false, "AUDIT": false,
"ADVANCED_GBT_AUDIT": false, "ADVANCED_GBT_AUDIT": false,
"ADVANCED_GBT_MEMPOOL": false, "ADVANCED_GBT_MEMPOOL": false,
"CPFP_INDEXING": false "CPFP_INDEXING": false,
"DISK_CACHE_BLOCK_INTERVAL": 6
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 8332, "PORT": 8332,
"USERNAME": "mempool", "USERNAME": "mempool",
"PASSWORD": "mempool" "PASSWORD": "mempool",
"TIMEOUT": 60000
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
@@ -47,7 +49,8 @@
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 8332, "PORT": 8332,
"USERNAME": "mempool", "USERNAME": "mempool",
"PASSWORD": "mempool" "PASSWORD": "mempool",
"TIMEOUT": 60000
}, },
"DATABASE": { "DATABASE": {
"ENABLED": true, "ENABLED": true,
@@ -91,7 +94,8 @@
"LND": { "LND": {
"TLS_CERT_PATH": "tls.cert", "TLS_CERT_PATH": "tls.cert",
"MACAROON_PATH": "readonly.macaroon", "MACAROON_PATH": "readonly.macaroon",
"REST_API_URL": "https://localhost:8080" "REST_API_URL": "https://localhost:8080",
"TIMEOUT": 10000
}, },
"CLIGHTNING": { "CLIGHTNING": {
"SOCKET": "lightning-rpc" "SOCKET": "lightning-rpc"

View File

@@ -28,13 +28,15 @@
"ADVANCED_GBT_AUDIT": "__MEMPOOL_ADVANCED_GBT_AUDIT__", "ADVANCED_GBT_AUDIT": "__MEMPOOL_ADVANCED_GBT_AUDIT__",
"ADVANCED_GBT_MEMPOOL": "__MEMPOOL_ADVANCED_GBT_MEMPOOL__", "ADVANCED_GBT_MEMPOOL": "__MEMPOOL_ADVANCED_GBT_MEMPOOL__",
"CPFP_INDEXING": "__MEMPOOL_CPFP_INDEXING__", "CPFP_INDEXING": "__MEMPOOL_CPFP_INDEXING__",
"MAX_BLOCKS_BULK_QUERY": "__MEMPOOL_MAX_BLOCKS_BULK_QUERY__" "MAX_BLOCKS_BULK_QUERY": "__MEMPOOL_MAX_BLOCKS_BULK_QUERY__",
"DISK_CACHE_BLOCK_INTERVAL": "__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__"
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "__CORE_RPC_HOST__", "HOST": "__CORE_RPC_HOST__",
"PORT": 15, "PORT": 15,
"USERNAME": "__CORE_RPC_USERNAME__", "USERNAME": "__CORE_RPC_USERNAME__",
"PASSWORD": "__CORE_RPC_PASSWORD__" "PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": "__CORE_RPC_TIMEOUT__"
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "__ELECTRUM_HOST__", "HOST": "__ELECTRUM_HOST__",
@@ -48,7 +50,8 @@
"HOST": "__SECOND_CORE_RPC_HOST__", "HOST": "__SECOND_CORE_RPC_HOST__",
"PORT": 17, "PORT": 17,
"USERNAME": "__SECOND_CORE_RPC_USERNAME__", "USERNAME": "__SECOND_CORE_RPC_USERNAME__",
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__" "PASSWORD": "__SECOND_CORE_RPC_PASSWORD__",
"TIMEOUT": "__SECOND_CORE_RPC_TIMEOUT__"
}, },
"DATABASE": { "DATABASE": {
"ENABLED": false, "ENABLED": false,
@@ -107,7 +110,8 @@
"LND": { "LND": {
"TLS_CERT_PATH": "", "TLS_CERT_PATH": "",
"MACAROON_PATH": "", "MACAROON_PATH": "",
"REST_API_URL": "https://localhost:8080" "REST_API_URL": "https://localhost:8080",
"TIMEOUT": 10000
}, },
"CLIGHTNING": { "CLIGHTNING": {
"SOCKET": "__CLIGHTNING_SOCKET__" "SOCKET": "__CLIGHTNING_SOCKET__"

View File

@@ -23,9 +23,11 @@ describe('Mempool Difficulty Adjustment', () => {
remainingBlocks: 1834, remainingBlocks: 1834,
remainingTime: 977591692, remainingTime: 977591692,
previousRetarget: 0.6280047707459726, previousRetarget: 0.6280047707459726,
previousTime: 1660820820,
nextRetargetHeight: 751968, nextRetargetHeight: 751968,
timeAvg: 533038, timeAvg: 533038,
timeOffset: 0, timeOffset: 0,
expectedBlocks: 161.68833333333333,
}, },
], ],
[ // Vector 2 (testnet) [ // Vector 2 (testnet)
@@ -43,11 +45,13 @@ describe('Mempool Difficulty Adjustment', () => {
estimatedRetargetDate: 1661895424692, estimatedRetargetDate: 1661895424692,
remainingBlocks: 1834, remainingBlocks: 1834,
remainingTime: 977591692, remainingTime: 977591692,
previousTime: 1660820820,
previousRetarget: 0.6280047707459726, previousRetarget: 0.6280047707459726,
nextRetargetHeight: 751968, nextRetargetHeight: 751968,
timeAvg: 533038, timeAvg: 533038,
timeOffset: -667000, // 11 min 7 seconds since last block (testnet only) timeOffset: -667000, // 11 min 7 seconds since last block (testnet only)
// If we add time avg to abs(timeOffset) it makes exactly 1200000 ms, or 20 minutes // If we add time avg to abs(timeOffset) it makes exactly 1200000 ms, or 20 minutes
expectedBlocks: 161.68833333333333,
}, },
], ],
] as [[number, number, number, number, string, number], DifficultyAdjustment][]; ] as [[number, number, number, number, string, number], DifficultyAdjustment][];

View File

@@ -42,6 +42,7 @@ describe('Mempool Backend Config', () => {
ADVANCED_GBT_MEMPOOL: false, ADVANCED_GBT_MEMPOOL: false,
CPFP_INDEXING: false, CPFP_INDEXING: false,
MAX_BLOCKS_BULK_QUERY: 0, MAX_BLOCKS_BULK_QUERY: 0,
DISK_CACHE_BLOCK_INTERVAL: 6,
}); });
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true }); expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
@@ -52,14 +53,16 @@ describe('Mempool Backend Config', () => {
HOST: '127.0.0.1', HOST: '127.0.0.1',
PORT: 8332, PORT: 8332,
USERNAME: 'mempool', USERNAME: 'mempool',
PASSWORD: 'mempool' PASSWORD: 'mempool',
TIMEOUT: 60000
}); });
expect(config.SECOND_CORE_RPC).toStrictEqual({ expect(config.SECOND_CORE_RPC).toStrictEqual({
HOST: '127.0.0.1', HOST: '127.0.0.1',
PORT: 8332, PORT: 8332,
USERNAME: 'mempool', USERNAME: 'mempool',
PASSWORD: 'mempool' PASSWORD: 'mempool',
TIMEOUT: 60000
}); });
expect(config.DATABASE).toStrictEqual({ expect(config.DATABASE).toStrictEqual({
@@ -106,6 +109,13 @@ describe('Mempool Backend Config', () => {
BISQ_URL: 'https://bisq.markets/api', BISQ_URL: 'https://bisq.markets/api',
BISQ_ONION: 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api' BISQ_ONION: 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
}); });
expect(config.MAXMIND).toStrictEqual({
ENABLED: false,
GEOLITE2_CITY: '/usr/local/share/GeoIP/GeoLite2-City.mmdb',
GEOLITE2_ASN: '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
GEOIP2_ISP: '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'
});
}); });
}); });

View File

@@ -5,9 +5,9 @@ const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first
class Audit { class Audit {
auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended }) auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended })
: { censored: string[], added: string[], fresh: string[], score: number } { : { censored: string[], added: string[], fresh: string[], score: number, similarity: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) { if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
return { censored: [], added: [], fresh: [], score: 0 }; return { censored: [], added: [], fresh: [], score: 0, similarity: 1 };
} }
const matches: string[] = []; // present in both mined block and template const matches: string[] = []; // present in both mined block and template
@@ -16,6 +16,8 @@ class Audit {
const isCensored = {}; // missing, without excuse const isCensored = {}; // missing, without excuse
const isDisplaced = {}; const isDisplaced = {};
let displacedWeight = 0; let displacedWeight = 0;
let matchedWeight = 0;
let projectedWeight = 0;
const inBlock = {}; const inBlock = {};
const inTemplate = {}; const inTemplate = {};
@@ -38,11 +40,16 @@ class Audit {
isCensored[txid] = true; isCensored[txid] = true;
} }
displacedWeight += mempool[txid].weight; displacedWeight += mempool[txid].weight;
} else {
matchedWeight += mempool[txid].weight;
} }
projectedWeight += mempool[txid].weight;
inTemplate[txid] = true; inTemplate[txid] = true;
} }
displacedWeight += (4000 - transactions[0].weight); displacedWeight += (4000 - transactions[0].weight);
projectedWeight += transactions[0].weight;
matchedWeight += transactions[0].weight;
// we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs // we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs
// these displaced transactions should occupy the first N weight units of the next projected block // these displaced transactions should occupy the first N weight units of the next projected block
@@ -121,12 +128,14 @@ class Audit {
const numCensored = Object.keys(isCensored).length; const numCensored = Object.keys(isCensored).length;
const numMatches = matches.length - 1; // adjust for coinbase tx const numMatches = matches.length - 1; // adjust for coinbase tx
const score = numMatches > 0 ? (numMatches / (numMatches + numCensored)) : 0; const score = numMatches > 0 ? (numMatches / (numMatches + numCensored)) : 0;
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
return { return {
censored: Object.keys(isCensored), censored: Object.keys(isCensored),
added, added,
fresh, fresh,
score score,
similarity,
}; };
} }
} }

View File

@@ -7,7 +7,7 @@ const nodeRpcCredentials: BitcoinRpcCredentials = {
port: config.CORE_RPC.PORT, port: config.CORE_RPC.PORT,
user: config.CORE_RPC.USERNAME, user: config.CORE_RPC.USERNAME,
pass: config.CORE_RPC.PASSWORD, pass: config.CORE_RPC.PASSWORD,
timeout: 60000, timeout: config.CORE_RPC.TIMEOUT,
}; };
export default new bitcoin.Client(nodeRpcCredentials); export default new bitcoin.Client(nodeRpcCredentials);

View File

@@ -7,7 +7,7 @@ const nodeRpcCredentials: BitcoinRpcCredentials = {
port: config.SECOND_CORE_RPC.PORT, port: config.SECOND_CORE_RPC.PORT,
user: config.SECOND_CORE_RPC.USERNAME, user: config.SECOND_CORE_RPC.USERNAME,
pass: config.SECOND_CORE_RPC.PASSWORD, pass: config.SECOND_CORE_RPC.PASSWORD,
timeout: 60000, timeout: config.SECOND_CORE_RPC.TIMEOUT,
}; };
export default new bitcoin.Client(nodeRpcCredentials); export default new bitcoin.Client(nodeRpcCredentials);

View File

@@ -220,18 +220,17 @@ class BitcoinRoutes {
let cpfpInfo; let cpfpInfo;
if (config.DATABASE.ENABLED) { if (config.DATABASE.ENABLED) {
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId); cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
}
if (cpfpInfo) {
res.json(cpfpInfo);
return;
} else { } else {
res.json({ res.json({
ancestors: [] ancestors: []
}); });
return; return;
} }
if (cpfpInfo) {
res.json(cpfpInfo);
return;
}
} }
res.status(404).send(`Transaction has no CPFP info available.`);
} }
private getBackendInfo(req: Request, res: Response) { private getBackendInfo(req: Request, res: Response) {
@@ -652,7 +651,7 @@ class BitcoinRoutes {
if (result) { if (result) {
res.json(result); res.json(result);
} else { } else {
res.status(404).send('not found'); res.status(204).send();
} }
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);

View File

@@ -143,7 +143,10 @@ class Blocks {
* @returns BlockSummary * @returns BlockSummary
*/ */
public summarizeBlock(block: IBitcoinApi.VerboseBlock): BlockSummary { public summarizeBlock(block: IBitcoinApi.VerboseBlock): BlockSummary {
const stripped = block.tx.map((tx) => { if (Common.isLiquid()) {
block = this.convertLiquidFees(block);
}
const stripped = block.tx.map((tx: IBitcoinApi.VerboseTransaction) => {
return { return {
txid: tx.txid, txid: tx.txid,
vsize: tx.weight / 4, vsize: tx.weight / 4,
@@ -158,6 +161,13 @@ class Blocks {
}; };
} }
private convertLiquidFees(block: IBitcoinApi.VerboseBlock): IBitcoinApi.VerboseBlock {
block.tx.forEach(tx => {
tx.fee = Object.values(tx.fee || {}).reduce((total, output) => total + output, 0);
});
return block;
}
/** /**
* Return a block with additional data (reward, coinbase, fees...) * Return a block with additional data (reward, coinbase, fees...)
* @param block * @param block
@@ -641,7 +651,7 @@ class Blocks {
if (this.newBlockCallbacks.length) { if (this.newBlockCallbacks.length) {
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions)); this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
} }
if (!memPool.hasPriority()) { if (!memPool.hasPriority() && (block.height % config.MEMPOOL.DISK_CACHE_BLOCK_INTERVAL === 0)) {
diskCache.$saveCacheToDisk(); diskCache.$saveCacheToDisk();
} }

View File

@@ -1,4 +1,4 @@
import { CpfpInfo, TransactionExtended, TransactionStripped } from '../mempool.interfaces'; import { CpfpInfo, MempoolBlockWithTransactions, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
import config from '../config'; import config from '../config';
import { NodeSocket } from '../repositories/NodesSocketsRepository'; import { NodeSocket } from '../repositories/NodesSocketsRepository';
import { isIP } from 'net'; import { isIP } from 'net';
@@ -164,6 +164,30 @@ export class Common {
return parents; return parents;
} }
// calculates the ratio of matched transactions to projected transactions by weight
static getSimilarity(projectedBlock: MempoolBlockWithTransactions, transactions: TransactionExtended[]): number {
let matchedWeight = 0;
let projectedWeight = 0;
const inBlock = {};
for (const tx of transactions) {
inBlock[tx.txid] = tx;
}
// look for transactions that were expected in the template, but missing from the mined block
for (const tx of projectedBlock.transactions) {
if (inBlock[tx.txid]) {
matchedWeight += tx.vsize * 4;
}
projectedWeight += tx.vsize * 4;
}
projectedWeight += transactions[0].weight;
matchedWeight += transactions[0].weight;
return projectedWeight ? matchedWeight / projectedWeight : 1;
}
static getSqlInterval(interval: string | null): string | null { static getSqlInterval(interval: string | null): string | null {
switch (interval) { switch (interval) {
case '24h': return '1 DAY'; case '24h': return '1 DAY';
@@ -175,6 +199,7 @@ export class Common {
case '1y': return '1 YEAR'; case '1y': return '1 YEAR';
case '2y': return '2 YEAR'; case '2y': return '2 YEAR';
case '3y': return '3 YEAR'; case '3y': return '3 YEAR';
case '4y': return '4 YEAR';
default: return null; default: return null;
} }
} }

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 58; private static currentVersion = 59;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@@ -510,6 +510,11 @@ class DatabaseMigration {
// We only run some migration queries for this version // We only run some migration queries for this version
await this.updateToSchemaVersion(58); await this.updateToSchemaVersion(58);
} }
if (databaseSchemaVersion < 59 && (config.MEMPOOL.NETWORK === 'signet' || config.MEMPOOL.NETWORK === 'testnet')) {
// https://github.com/mempool/mempool/issues/3360
await this.$executeQuery(`TRUNCATE prices`);
}
} }
/** /**
@@ -1037,7 +1042,7 @@ class DatabaseMigration {
await this.$executeQuery('DELETE FROM `pools`'); await this.$executeQuery('DELETE FROM `pools`');
await this.$executeQuery('ALTER TABLE pools AUTO_INCREMENT = 1'); await this.$executeQuery('ALTER TABLE pools AUTO_INCREMENT = 1');
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`); await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
} }
private async $convertCompactCpfpTables(): Promise<void> { private async $convertCompactCpfpTables(): Promise<void> {
try { try {

View File

@@ -9,9 +9,11 @@ export interface DifficultyAdjustment {
remainingBlocks: number; // Block count remainingBlocks: number; // Block count
remainingTime: number; // Duration of time in ms remainingTime: number; // Duration of time in ms
previousRetarget: number; // Percent: -75 to 300 previousRetarget: number; // Percent: -75 to 300
previousTime: number; // Unix time in ms
nextRetargetHeight: number; // Block Height nextRetargetHeight: number; // Block Height
timeAvg: number; // Duration of time in ms timeAvg: number; // Duration of time in ms
timeOffset: number; // (Testnet) Time since last block (cap @ 20min) in ms timeOffset: number; // (Testnet) Time since last block (cap @ 20min) in ms
expectedBlocks: number; // Block count
} }
export function calcDifficultyAdjustment( export function calcDifficultyAdjustment(
@@ -32,12 +34,12 @@ export function calcDifficultyAdjustment(
const progressPercent = (blockHeight >= 0) ? blocksInEpoch / EPOCH_BLOCK_LENGTH * 100 : 100; const progressPercent = (blockHeight >= 0) ? blocksInEpoch / EPOCH_BLOCK_LENGTH * 100 : 100;
const remainingBlocks = EPOCH_BLOCK_LENGTH - blocksInEpoch; const remainingBlocks = EPOCH_BLOCK_LENGTH - blocksInEpoch;
const nextRetargetHeight = (blockHeight >= 0) ? blockHeight + remainingBlocks : 0; const nextRetargetHeight = (blockHeight >= 0) ? blockHeight + remainingBlocks : 0;
const expectedBlocks = diffSeconds / BLOCK_SECONDS_TARGET;
let difficultyChange = 0; let difficultyChange = 0;
let timeAvgSecs = BLOCK_SECONDS_TARGET; let timeAvgSecs = diffSeconds / blocksInEpoch;
// Only calculate the estimate once we have 7.2% of blocks in current epoch // Only calculate the estimate once we have 7.2% of blocks in current epoch
if (blocksInEpoch >= ESTIMATE_LAG_BLOCKS) { if (blocksInEpoch >= ESTIMATE_LAG_BLOCKS) {
timeAvgSecs = diffSeconds / blocksInEpoch;
difficultyChange = (BLOCK_SECONDS_TARGET / timeAvgSecs - 1) * 100; difficultyChange = (BLOCK_SECONDS_TARGET / timeAvgSecs - 1) * 100;
// Max increase is x4 (+300%) // Max increase is x4 (+300%)
if (difficultyChange > 300) { if (difficultyChange > 300) {
@@ -74,9 +76,11 @@ export function calcDifficultyAdjustment(
remainingBlocks, remainingBlocks,
remainingTime, remainingTime,
previousRetarget, previousRetarget,
previousTime: DATime,
nextRetargetHeight, nextRetargetHeight,
timeAvg, timeAvg,
timeOffset, timeOffset,
expectedBlocks,
}; };
} }

View File

@@ -11,23 +11,33 @@ import { Common } from './common';
class DiskCache { class DiskCache {
private cacheSchemaVersion = 3; private cacheSchemaVersion = 3;
private static TMP_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/tmp-cache.json';
private static TMP_FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/tmp-cache{number}.json';
private static FILE_NAME = config.MEMPOOL.CACHE_DIR + '/cache.json'; private static FILE_NAME = config.MEMPOOL.CACHE_DIR + '/cache.json';
private static FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/cache{number}.json'; private static FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/cache{number}.json';
private static CHUNK_FILES = 25; private static CHUNK_FILES = 25;
private isWritingCache = false; private isWritingCache = false;
constructor() { } constructor() {
if (!cluster.isPrimary) {
return;
}
process.on('SIGINT', (e) => {
this.$saveCacheToDisk(true);
process.exit(0);
});
}
async $saveCacheToDisk(): Promise<void> { async $saveCacheToDisk(sync: boolean = false): Promise<void> {
if (!cluster.isPrimary) { if (!cluster.isPrimary) {
return; return;
} }
if (this.isWritingCache) { if (this.isWritingCache) {
logger.debug('Saving cache already in progress. Skipping.') logger.debug('Saving cache already in progress. Skipping.');
return; return;
} }
try { try {
logger.debug('Writing mempool and blocks data to disk cache (async)...'); logger.debug(`Writing mempool and blocks data to disk cache (${ sync ? 'sync' : 'async' })...`);
this.isWritingCache = true; this.isWritingCache = true;
const mempool = memPool.getMempool(); const mempool = memPool.getMempool();
@@ -40,19 +50,48 @@ class DiskCache {
const chunkSize = Math.floor(mempoolArray.length / DiskCache.CHUNK_FILES); const chunkSize = Math.floor(mempoolArray.length / DiskCache.CHUNK_FILES);
await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({ if (sync) {
cacheSchemaVersion: this.cacheSchemaVersion, fs.writeFileSync(DiskCache.TMP_FILE_NAME, JSON.stringify({
blocks: blocks.getBlocks(), network: config.MEMPOOL.NETWORK,
blockSummaries: blocks.getBlockSummaries(), cacheSchemaVersion: this.cacheSchemaVersion,
mempool: {}, blocks: blocks.getBlocks(),
mempoolArray: mempoolArray.splice(0, chunkSize), blockSummaries: blocks.getBlockSummaries(),
}), { flag: 'w' });
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
await fsPromises.writeFile(DiskCache.FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
mempool: {}, mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize), mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' }); }), { flag: 'w' });
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
fs.writeFileSync(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' });
}
fs.renameSync(DiskCache.TMP_FILE_NAME, DiskCache.FILE_NAME);
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
fs.renameSync(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), DiskCache.FILE_NAMES.replace('{number}', i.toString()));
}
} else {
await fsPromises.writeFile(DiskCache.TMP_FILE_NAME, JSON.stringify({
network: config.MEMPOOL.NETWORK,
cacheSchemaVersion: this.cacheSchemaVersion,
blocks: blocks.getBlocks(),
blockSummaries: blocks.getBlockSummaries(),
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' });
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
await fsPromises.writeFile(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' });
}
await fsPromises.rename(DiskCache.TMP_FILE_NAME, DiskCache.FILE_NAME);
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
await fsPromises.rename(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), DiskCache.FILE_NAMES.replace('{number}', i.toString()));
}
} }
logger.debug('Mempool and blocks data saved to disk cache'); logger.debug('Mempool and blocks data saved to disk cache');
this.isWritingCache = false; this.isWritingCache = false;
} catch (e) { } catch (e) {
@@ -61,8 +100,8 @@ class DiskCache {
} }
} }
wipeCache() { wipeCache(): void {
logger.notice(`Wipping nodejs backend cache/cache*.json files`); logger.notice(`Wiping nodejs backend cache/cache*.json files`);
try { try {
fs.unlinkSync(DiskCache.FILE_NAME); fs.unlinkSync(DiskCache.FILE_NAME);
} catch (e: any) { } catch (e: any) {
@@ -83,7 +122,7 @@ class DiskCache {
} }
} }
loadMempoolCache() { loadMempoolCache(): void {
if (!fs.existsSync(DiskCache.FILE_NAME)) { if (!fs.existsSync(DiskCache.FILE_NAME)) {
return; return;
} }
@@ -97,6 +136,10 @@ class DiskCache {
logger.notice('Disk cache contains an outdated schema version. Clearing it and skipping the cache loading.'); logger.notice('Disk cache contains an outdated schema version. Clearing it and skipping the cache loading.');
return this.wipeCache(); return this.wipeCache();
} }
if (data.network && data.network !== config.MEMPOOL.NETWORK) {
logger.notice('Disk cache contains data from a different network. Clearing it and skipping the cache loading.');
return this.wipeCache();
}
if (data.mempoolArray) { if (data.mempoolArray) {
for (const tx of data.mempoolArray) { for (const tx of data.mempoolArray) {

View File

@@ -417,24 +417,24 @@ class NodesApi {
if (!ispList[isp1]) { if (!ispList[isp1]) {
ispList[isp1] = { ispList[isp1] = {
id: channel.isp1ID.toString(), ids: [channel.isp1ID],
capacity: 0, capacity: 0,
channels: 0, channels: 0,
nodes: {}, nodes: {},
}; };
} else if (ispList[isp1].id.indexOf(channel.isp1ID) === -1) { } else if (ispList[isp1].ids.includes(channel.isp1ID) === false) {
ispList[isp1].id += ',' + channel.isp1ID.toString(); ispList[isp1].ids.push(channel.isp1ID);
} }
if (!ispList[isp2]) { if (!ispList[isp2]) {
ispList[isp2] = { ispList[isp2] = {
id: channel.isp2ID.toString(), ids: [channel.isp2ID],
capacity: 0, capacity: 0,
channels: 0, channels: 0,
nodes: {}, nodes: {},
}; };
} else if (ispList[isp2].id.indexOf(channel.isp2ID) === -1) { } else if (ispList[isp2].ids.includes(channel.isp2ID) === false) {
ispList[isp2].id += ',' + channel.isp2ID.toString(); ispList[isp2].ids.push(channel.isp2ID);
} }
ispList[isp1].capacity += channel.capacity; ispList[isp1].capacity += channel.capacity;
@@ -444,11 +444,11 @@ class NodesApi {
ispList[isp2].channels += 1; ispList[isp2].channels += 1;
ispList[isp2].nodes[channel.node2PublicKey] = true; ispList[isp2].nodes[channel.node2PublicKey] = true;
} }
const ispRanking: any[] = []; const ispRanking: any[] = [];
for (const isp of Object.keys(ispList)) { for (const isp of Object.keys(ispList)) {
ispRanking.push([ ispRanking.push([
ispList[isp].id, ispList[isp].ids.sort((a, b) => a - b).join(','),
isp, isp,
ispList[isp].capacity, ispList[isp].capacity,
ispList[isp].channels, ispList[isp].channels,

View File

@@ -4,21 +4,29 @@ import * as fs from 'fs';
import { AbstractLightningApi } from '../lightning-api-abstract-factory'; import { AbstractLightningApi } from '../lightning-api-abstract-factory';
import { ILightningApi } from '../lightning-api.interface'; import { ILightningApi } from '../lightning-api.interface';
import config from '../../../config'; import config from '../../../config';
import logger from '../../../logger';
class LndApi implements AbstractLightningApi { class LndApi implements AbstractLightningApi {
axiosConfig: AxiosRequestConfig = {}; axiosConfig: AxiosRequestConfig = {};
constructor() { constructor() {
if (config.LIGHTNING.ENABLED) { if (!config.LIGHTNING.ENABLED) {
return;
}
try {
this.axiosConfig = { this.axiosConfig = {
headers: { headers: {
'Grpc-Metadata-macaroon': fs.readFileSync(config.LND.MACAROON_PATH).toString('hex') 'Grpc-Metadata-macaroon': fs.readFileSync(config.LND.MACAROON_PATH).toString('hex'),
}, },
httpsAgent: new Agent({ httpsAgent: new Agent({
ca: fs.readFileSync(config.LND.TLS_CERT_PATH) ca: fs.readFileSync(config.LND.TLS_CERT_PATH)
}), }),
timeout: 10000 timeout: config.LND.TIMEOUT
}; };
} catch (e) {
config.LIGHTNING.ENABLED = false;
logger.updateNetwork();
logger.err(`Could not initialize LND Macaroon/TLS Cert. Disabling LIGHTNING. ` + (e instanceof Error ? e.message : e));
} }
} }

View File

@@ -31,6 +31,11 @@ class Mempool {
private mempoolProtection = 0; private mempoolProtection = 0;
private latestTransactions: any[] = []; private latestTransactions: any[] = [];
private ESPLORA_MISSING_TX_WARNING_THRESHOLD = 100;
private SAMPLE_TIME = 10000; // In ms
private timer = new Date().getTime();
private missingTxCount = 0;
constructor() { constructor() {
setInterval(this.updateTxPerSecond.bind(this), 1000); setInterval(this.updateTxPerSecond.bind(this), 1000);
setInterval(this.deleteExpiredTransactions.bind(this), 20000); setInterval(this.deleteExpiredTransactions.bind(this), 20000);
@@ -128,6 +133,16 @@ class Mempool {
loadingIndicators.setProgress('mempool', Object.keys(this.mempoolCache).length / transactions.length * 100); loadingIndicators.setProgress('mempool', Object.keys(this.mempoolCache).length / transactions.length * 100);
} }
// https://github.com/mempool/mempool/issues/3283
const logEsplora404 = (missingTxCount, threshold, time) => {
const log = `In the past ${time / 1000} seconds, esplora tx API replied ${missingTxCount} times with a 404 error code while updating nodejs backend mempool`;
if (missingTxCount >= threshold) {
logger.warn(log);
} else if (missingTxCount > 0) {
logger.debug(log);
}
};
for (const txid of transactions) { for (const txid of transactions) {
if (!this.mempoolCache[txid]) { if (!this.mempoolCache[txid]) {
try { try {
@@ -142,7 +157,10 @@ class Mempool {
} }
hasChange = true; hasChange = true;
newTransactions.push(transaction); newTransactions.push(transaction);
} catch (e) { } catch (e: any) {
if (config.MEMPOOL.BACKEND === 'esplora' && e.response?.status === 404) {
this.missingTxCount++;
}
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e)); logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
} }
} }
@@ -152,6 +170,14 @@ class Mempool {
} }
} }
// Reset esplora 404 counter and log a warning if needed
const elapsedTime = new Date().getTime() - this.timer;
if (elapsedTime > this.SAMPLE_TIME) {
logEsplora404(this.missingTxCount, this.ESPLORA_MISSING_TX_WARNING_THRESHOLD, elapsedTime);
this.timer = new Date().getTime();
this.missingTxCount = 0;
}
// Prevent mempool from clear on bitcoind restart by delaying the deletion // Prevent mempool from clear on bitcoind restart by delaying the deletion
if (this.mempoolProtection === 0 if (this.mempoolProtection === 0
&& currentMempoolSize > 20000 && currentMempoolSize > 20000

View File

@@ -263,7 +263,7 @@ class MiningRoutes {
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash); const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
if (!audit) { if (!audit) {
res.status(404).send(`This block has not been audited.`); res.status(204).send(`This block has not been audited.`);
return; return;
} }

View File

@@ -13,6 +13,7 @@ import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
import PricesRepository from '../../repositories/PricesRepository'; import PricesRepository from '../../repositories/PricesRepository';
import { bitcoinCoreApi } from '../bitcoin/bitcoin-api-factory'; import { bitcoinCoreApi } from '../bitcoin/bitcoin-api-factory';
import { IEsploraApi } from '../bitcoin/esplora-api.interface'; import { IEsploraApi } from '../bitcoin/esplora-api.interface';
import database from '../../database';
class Mining { class Mining {
private blocksPriceIndexingRunning = false; private blocksPriceIndexingRunning = false;
@@ -117,7 +118,7 @@ class Mining {
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h); poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
} catch (e) { } catch (e) {
poolsStatistics['lastEstimatedHashrate'] = 0; poolsStatistics['lastEstimatedHashrate'] = 0;
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate'); logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);
} }
return poolsStatistics; return poolsStatistics;
@@ -141,11 +142,14 @@ class Mining {
const blockCount1w: number = await BlocksRepository.$blockCount(pool.id, '1w'); const blockCount1w: number = await BlocksRepository.$blockCount(pool.id, '1w');
const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w'); const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w');
const avgHealth = await BlocksRepository.$getAvgBlockHealthPerPoolId(pool.id);
const totalReward = await BlocksRepository.$getTotalRewardForPoolId(pool.id);
let currentEstimatedHashrate = 0; let currentEstimatedHashrate = 0;
try { try {
currentEstimatedHashrate = await bitcoinClient.getNetworkHashPs(totalBlock24h); currentEstimatedHashrate = await bitcoinClient.getNetworkHashPs(totalBlock24h);
} catch (e) { } catch (e) {
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate'); logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);
} }
return { return {
@@ -162,6 +166,8 @@ class Mining {
}, },
estimatedHashrate: currentEstimatedHashrate * (blockCount24h / totalBlock24h), estimatedHashrate: currentEstimatedHashrate * (blockCount24h / totalBlock24h),
reportedHashrate: null, reportedHashrate: null,
avgBlockHealth: avgHealth,
totalReward: totalReward,
}; };
} }
@@ -208,7 +214,7 @@ class Mining {
const startedAt = new Date().getTime() / 1000; const startedAt = new Date().getTime() / 1000;
let timer = new Date().getTime() / 1000; let timer = new Date().getTime() / 1000;
logger.debug(`Indexing weekly mining pool hashrate`); logger.debug(`Indexing weekly mining pool hashrate`, logger.tags.mining);
loadingIndicators.setProgress('weekly-hashrate-indexing', 0); loadingIndicators.setProgress('weekly-hashrate-indexing', 0);
while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) { while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
@@ -245,7 +251,7 @@ class Mining {
}); });
} }
newlyIndexed += hashrates.length; newlyIndexed += hashrates.length / Math.max(1, pools.length);
await HashratesRepository.$saveHashrates(hashrates); await HashratesRepository.$saveHashrates(hashrates);
hashrates.length = 0; hashrates.length = 0;
} }
@@ -256,7 +262,7 @@ class Mining {
const weeksPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds)); const weeksPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totalIndexed / totalWeekIndexed * 10000) / 100; const progress = Math.round(totalIndexed / totalWeekIndexed * 10000) / 100;
const formattedDate = new Date(fromTimestamp).toUTCString(); const formattedDate = new Date(fromTimestamp).toUTCString();
logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds.toFixed(2)} weeks/sec | total: ~${totalIndexed}/${Math.round(totalWeekIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`); logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds.toFixed(2)} weeks/sec | total: ~${totalIndexed}/${Math.round(totalWeekIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
timer = new Date().getTime() / 1000; timer = new Date().getTime() / 1000;
indexedThisRun = 0; indexedThisRun = 0;
loadingIndicators.setProgress('weekly-hashrate-indexing', progress, false); loadingIndicators.setProgress('weekly-hashrate-indexing', progress, false);
@@ -268,14 +274,14 @@ class Mining {
} }
this.lastWeeklyHashrateIndexingDate = new Date().getUTCDate(); this.lastWeeklyHashrateIndexingDate = new Date().getUTCDate();
if (newlyIndexed > 0) { if (newlyIndexed > 0) {
logger.notice(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`, logger.tags.mining); logger.info(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed} weeks`, logger.tags.mining);
} else { } else {
logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`, logger.tags.mining); logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed} weeks`, logger.tags.mining);
} }
loadingIndicators.setProgress('weekly-hashrate-indexing', 100); loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
} catch (e) { } catch (e) {
loadingIndicators.setProgress('weekly-hashrate-indexing', 100); loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
logger.err(`Weekly mining pools hashrates indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`); logger.err(`Weekly mining pools hashrates indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining);
throw e; throw e;
} }
} }
@@ -308,7 +314,7 @@ class Mining {
const startedAt = new Date().getTime() / 1000; const startedAt = new Date().getTime() / 1000;
let timer = new Date().getTime() / 1000; let timer = new Date().getTime() / 1000;
logger.debug(`Indexing daily network hashrate`); logger.debug(`Indexing daily network hashrate`, logger.tags.mining);
loadingIndicators.setProgress('daily-hashrate-indexing', 0); loadingIndicators.setProgress('daily-hashrate-indexing', 0);
while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) { while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
@@ -346,7 +352,7 @@ class Mining {
const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds)); const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totalIndexed / totalDayIndexed * 10000) / 100; const progress = Math.round(totalIndexed / totalDayIndexed * 10000) / 100;
const formattedDate = new Date(fromTimestamp).toUTCString(); const formattedDate = new Date(fromTimestamp).toUTCString();
logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds.toFixed(2)} days/sec | total: ~${totalIndexed}/${Math.round(totalDayIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`); logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds.toFixed(2)} days/sec | total: ~${totalIndexed}/${Math.round(totalDayIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
timer = new Date().getTime() / 1000; timer = new Date().getTime() / 1000;
indexedThisRun = 0; indexedThisRun = 0;
loadingIndicators.setProgress('daily-hashrate-indexing', progress); loadingIndicators.setProgress('daily-hashrate-indexing', progress);
@@ -373,14 +379,14 @@ class Mining {
this.lastHashrateIndexingDate = new Date().getUTCDate(); this.lastHashrateIndexingDate = new Date().getUTCDate();
if (newlyIndexed > 0) { if (newlyIndexed > 0) {
logger.notice(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining); logger.info(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining);
} else { } else {
logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining); logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining);
} }
loadingIndicators.setProgress('daily-hashrate-indexing', 100); loadingIndicators.setProgress('daily-hashrate-indexing', 100);
} catch (e) { } catch (e) {
loadingIndicators.setProgress('daily-hashrate-indexing', 100); loadingIndicators.setProgress('daily-hashrate-indexing', 100);
logger.err(`Daily network hashrate indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining); logger.err(`Daily network hashrate indexing failed. Trying again later. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining);
throw e; throw e;
} }
} }
@@ -446,13 +452,13 @@ class Mining {
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer)); const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
if (elapsedSeconds > 5) { if (elapsedSeconds > 5) {
const progress = Math.round(totalBlockChecked / blocks.length * 100); const progress = Math.round(totalBlockChecked / blocks.length * 100);
logger.info(`Indexing difficulty adjustment at block #${block.height} | Progress: ${progress}%`); logger.info(`Indexing difficulty adjustment at block #${block.height} | Progress: ${progress}%`, logger.tags.mining);
timer = new Date().getTime() / 1000; timer = new Date().getTime() / 1000;
} }
} }
if (totalIndexed > 0) { if (totalIndexed > 0) {
logger.notice(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining); logger.info(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining);
} else { } else {
logger.debug(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining); logger.debug(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining);
} }
@@ -499,7 +505,7 @@ class Mining {
if (blocksWithoutPrices.length > 200000) { if (blocksWithoutPrices.length > 200000) {
logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`; logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`;
} }
logger.debug(logStr); logger.debug(logStr, logger.tags.mining);
await BlocksRepository.$saveBlockPrices(blocksPrices); await BlocksRepository.$saveBlockPrices(blocksPrices);
blocksPrices.length = 0; blocksPrices.length = 0;
} }
@@ -511,7 +517,7 @@ class Mining {
if (blocksWithoutPrices.length > 200000) { if (blocksWithoutPrices.length > 200000) {
logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`; logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`;
} }
logger.debug(logStr); logger.debug(logStr, logger.tags.mining);
await BlocksRepository.$saveBlockPrices(blocksPrices); await BlocksRepository.$saveBlockPrices(blocksPrices);
} }
} catch (e) { } catch (e) {
@@ -568,6 +574,7 @@ class Mining {
private getTimeRange(interval: string | null, scale = 1): number { private getTimeRange(interval: string | null, scale = 1): number {
switch (interval) { switch (interval) {
case '4y': return 43200 * scale; // 12h
case '3y': return 43200 * scale; // 12h case '3y': return 43200 * scale; // 12h
case '2y': return 28800 * scale; // 8h case '2y': return 28800 * scale; // 8h
case '1y': return 28800 * scale; // 8h case '1y': return 28800 * scale; // 8h

View File

@@ -375,6 +375,17 @@ class StatisticsApi {
} }
} }
public async $list4Y(): Promise<OptimizedStatistic[]> {
try {
const query = this.getQueryForDays(43200, '4 YEAR'); // 12h interval
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
} catch (e) {
logger.err('$list4Y() error' + (e instanceof Error ? e.message : e));
return [];
}
}
private mapStatisticToOptimizedStatistic(statistic: Statistic[]): OptimizedStatistic[] { private mapStatisticToOptimizedStatistic(statistic: Statistic[]): OptimizedStatistic[] {
return statistic.map((s) => { return statistic.map((s) => {
return { return {

View File

@@ -14,10 +14,11 @@ class StatisticsRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', this.$getStatisticsByTime.bind(this, '1y')) .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', this.$getStatisticsByTime.bind(this, '1y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', this.$getStatisticsByTime.bind(this, '2y')) .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', this.$getStatisticsByTime.bind(this, '2y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', this.$getStatisticsByTime.bind(this, '3y')) .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', this.$getStatisticsByTime.bind(this, '3y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/4y', this.$getStatisticsByTime.bind(this, '4y'))
; ;
} }
private async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y', req: Request, res: Response) { private async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y' | '4y', req: Request, res: Response) {
res.header('Pragma', 'public'); res.header('Pragma', 'public');
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
@@ -54,6 +55,9 @@ class StatisticsRoutes {
case '3y': case '3y':
result = await statisticsApi.$list3Y(); result = await statisticsApi.$list3Y();
break; break;
case '4y':
result = await statisticsApi.$list4Y();
break;
default: default:
result = await statisticsApi.$list2H(); result = await statisticsApi.$list2H();
} }

View File

@@ -1,8 +1,8 @@
import logger from '../logger'; import logger from '../logger';
import * as WebSocket from 'ws'; import * as WebSocket from 'ws';
import { import {
BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock, MempoolBlockDelta, BlockExtended, TransactionExtended, WebsocketResponse,
OptimizedStatistic, ILoadingIndicators, IConversionRates OptimizedStatistic, ILoadingIndicators
} from '../mempool.interfaces'; } from '../mempool.interfaces';
import blocks from './blocks'; import blocks from './blocks';
import memPool from './mempool'; import memPool from './mempool';
@@ -20,6 +20,7 @@ import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository
import Audit from './audit'; import Audit from './audit';
import { deepClone } from '../utils/clone'; import { deepClone } from '../utils/clone';
import priceUpdater from '../tasks/price-updater'; import priceUpdater from '../tasks/price-updater';
import { ApiPrice } from '../repositories/PricesRepository';
class WebsocketHandler { class WebsocketHandler {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
@@ -193,7 +194,7 @@ class WebsocketHandler {
}); });
} }
handleNewConversionRates(conversionRates: IConversionRates) { handleNewConversionRates(conversionRates: ApiPrice) {
if (!this.wss) { if (!this.wss) {
throw new Error('WebSocket.Server is not set'); throw new Error('WebSocket.Server is not set');
} }
@@ -214,7 +215,7 @@ class WebsocketHandler {
'mempoolInfo': memPool.getMempoolInfo(), 'mempoolInfo': memPool.getMempoolInfo(),
'vBytesPerSecond': memPool.getVBytesPerSecond(), 'vBytesPerSecond': memPool.getVBytesPerSecond(),
'blocks': _blocks, 'blocks': _blocks,
'conversions': priceUpdater.latestPrices, 'conversions': priceUpdater.getLatestPrices(),
'mempool-blocks': mempoolBlocks.getMempoolBlocks(), 'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
'transactions': memPool.getLatestTransactions(), 'transactions': memPool.getLatestTransactions(),
'backendInfo': backendInfo.getBackendInfo(), 'backendInfo': backendInfo.getBackendInfo(),
@@ -431,7 +432,7 @@ class WebsocketHandler {
} }
if (Common.indexingEnabled() && memPool.isInSync()) { if (Common.indexingEnabled() && memPool.isInSync()) {
const { censored, added, fresh, score } = Audit.auditBlock(transactions, projectedBlocks, auditMempool); const { censored, added, fresh, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
const matchRate = Math.round(score * 100 * 100) / 100; const matchRate = Math.round(score * 100 * 100) / 100;
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => { const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
@@ -463,8 +464,14 @@ class WebsocketHandler {
if (block.extras) { if (block.extras) {
block.extras.matchRate = matchRate; block.extras.matchRate = matchRate;
block.extras.similarity = similarity;
} }
} }
} else if (block.extras) {
const mBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
if (mBlocks?.length && mBlocks[0].transactions) {
block.extras.similarity = Common.getSimilarity(mBlocks[0], transactions);
}
} }
const removed: string[] = []; const removed: string[] = [];

View File

@@ -33,6 +33,7 @@ interface IConfig {
ADVANCED_GBT_MEMPOOL: boolean; ADVANCED_GBT_MEMPOOL: boolean;
CPFP_INDEXING: boolean; CPFP_INDEXING: boolean;
MAX_BLOCKS_BULK_QUERY: number; MAX_BLOCKS_BULK_QUERY: number;
DISK_CACHE_BLOCK_INTERVAL: number;
}; };
ESPLORA: { ESPLORA: {
REST_API_URL: string; REST_API_URL: string;
@@ -51,6 +52,7 @@ interface IConfig {
TLS_CERT_PATH: string; TLS_CERT_PATH: string;
MACAROON_PATH: string; MACAROON_PATH: string;
REST_API_URL: string; REST_API_URL: string;
TIMEOUT: number;
}; };
CLIGHTNING: { CLIGHTNING: {
SOCKET: string; SOCKET: string;
@@ -65,12 +67,14 @@ interface IConfig {
PORT: number; PORT: number;
USERNAME: string; USERNAME: string;
PASSWORD: string; PASSWORD: string;
TIMEOUT: number;
}; };
SECOND_CORE_RPC: { SECOND_CORE_RPC: {
HOST: string; HOST: string;
PORT: number; PORT: number;
USERNAME: string; USERNAME: string;
PASSWORD: string; PASSWORD: string;
TIMEOUT: number;
}; };
DATABASE: { DATABASE: {
ENABLED: boolean; ENABLED: boolean;
@@ -155,6 +159,7 @@ const defaults: IConfig = {
'ADVANCED_GBT_MEMPOOL': false, 'ADVANCED_GBT_MEMPOOL': false,
'CPFP_INDEXING': false, 'CPFP_INDEXING': false,
'MAX_BLOCKS_BULK_QUERY': 0, 'MAX_BLOCKS_BULK_QUERY': 0,
'DISK_CACHE_BLOCK_INTERVAL': 6,
}, },
'ESPLORA': { 'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000', 'REST_API_URL': 'http://127.0.0.1:3000',
@@ -168,13 +173,15 @@ const defaults: IConfig = {
'HOST': '127.0.0.1', 'HOST': '127.0.0.1',
'PORT': 8332, 'PORT': 8332,
'USERNAME': 'mempool', 'USERNAME': 'mempool',
'PASSWORD': 'mempool' 'PASSWORD': 'mempool',
'TIMEOUT': 60000,
}, },
'SECOND_CORE_RPC': { 'SECOND_CORE_RPC': {
'HOST': '127.0.0.1', 'HOST': '127.0.0.1',
'PORT': 8332, 'PORT': 8332,
'USERNAME': 'mempool', 'USERNAME': 'mempool',
'PASSWORD': 'mempool' 'PASSWORD': 'mempool',
'TIMEOUT': 60000,
}, },
'DATABASE': { 'DATABASE': {
'ENABLED': true, 'ENABLED': true,
@@ -214,6 +221,7 @@ const defaults: IConfig = {
'TLS_CERT_PATH': '', 'TLS_CERT_PATH': '',
'MACAROON_PATH': '', 'MACAROON_PATH': '',
'REST_API_URL': 'https://localhost:8080', 'REST_API_URL': 'https://localhost:8080',
'TIMEOUT': 10000,
}, },
'CLIGHTNING': { 'CLIGHTNING': {
'SOCKET': '', 'SOCKET': '',

View File

@@ -38,6 +38,8 @@ import forensicsService from './tasks/lightning/forensics.service';
import priceUpdater from './tasks/price-updater'; import priceUpdater from './tasks/price-updater';
import chainTips from './api/chain-tips'; import chainTips from './api/chain-tips';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import v8 from 'v8';
import { formatBytes, getBytesUnit } from './utils/format';
class Server { class Server {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
@@ -45,6 +47,11 @@ class Server {
private app: Application; private app: Application;
private currentBackendRetryInterval = 5; private currentBackendRetryInterval = 5;
private maxHeapSize: number = 0;
private heapLogInterval: number = 60;
private warnedHeapCritical: boolean = false;
private lastHeapLogTime: number | null = null;
constructor() { constructor() {
this.app = express(); this.app = express();
@@ -137,6 +144,8 @@ class Server {
this.runMainUpdateLoop(); this.runMainUpdateLoop();
} }
setInterval(() => { this.healthCheck(); }, 2500);
if (config.BISQ.ENABLED) { if (config.BISQ.ENABLED) {
bisq.startBisqService(); bisq.startBisqService();
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price)); bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price));
@@ -206,11 +215,11 @@ class Server {
await lightningStatsUpdater.$startService(); await lightningStatsUpdater.$startService();
await forensicsService.$startService(); await forensicsService.$startService();
} catch(e) { } catch(e) {
logger.err(`Nodejs lightning backend crashed. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`); logger.err(`Exception in $runLightningBackend. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`);
await Common.sleep$(1000 * 60); await Common.sleep$(1000 * 60);
this.$runLightningBackend(); this.$runLightningBackend();
}; };
} }
setUpWebsocketHandling(): void { setUpWebsocketHandling(): void {
if (this.wss) { if (this.wss) {
@@ -255,6 +264,26 @@ class Server {
channelsRoutes.initRoutes(this.app); channelsRoutes.initRoutes(this.app);
} }
} }
healthCheck(): void {
const now = Date.now();
const stats = v8.getHeapStatistics();
this.maxHeapSize = Math.max(stats.used_heap_size, this.maxHeapSize);
const warnThreshold = 0.8 * stats.heap_size_limit;
const byteUnits = getBytesUnit(Math.max(this.maxHeapSize, stats.heap_size_limit));
if (!this.warnedHeapCritical && this.maxHeapSize > warnThreshold) {
this.warnedHeapCritical = true;
logger.warn(`Used ${(this.maxHeapSize / stats.heap_size_limit).toFixed(2)}% of heap limit (${formatBytes(this.maxHeapSize, byteUnits, true)} / ${formatBytes(stats.heap_size_limit, byteUnits)})!`);
}
if (this.lastHeapLogTime === null || (now - this.lastHeapLogTime) > (this.heapLogInterval * 1000)) {
logger.debug(`Memory usage: ${formatBytes(this.maxHeapSize, byteUnits)} / ${formatBytes(stats.heap_size_limit, byteUnits)}`);
this.warnedHeapCritical = false;
this.maxHeapSize = 0;
this.lastHeapLogTime = now;
}
}
} }
((): Server => new Server())(); ((): Server => new Server())();

View File

@@ -76,13 +76,13 @@ class Indexer {
this.tasksRunning.push(task); this.tasksRunning.push(task);
const lastestPriceId = await PricesRepository.$getLatestPriceId(); const lastestPriceId = await PricesRepository.$getLatestPriceId();
if (priceUpdater.historyInserted === false || lastestPriceId === null) { if (priceUpdater.historyInserted === false || lastestPriceId === null) {
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`); logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining);
setTimeout(() => { setTimeout(() => {
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task); this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
this.runSingleTask('blocksPrices'); this.runSingleTask('blocksPrices');
}, 10000); }, 10000);
} else { } else {
logger.debug(`Blocks prices indexer will run now`); logger.debug(`Blocks prices indexer will run now`, logger.tags.mining);
await mining.$indexBlockPrices(); await mining.$indexBlockPrices();
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task); this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
} }
@@ -112,7 +112,7 @@ class Indexer {
this.runIndexer = false; this.runIndexer = false;
this.indexerRunning = true; this.indexerRunning = true;
logger.info(`Running mining indexer`); logger.debug(`Running mining indexer`);
await this.checkAvailableCoreIndexes(); await this.checkAvailableCoreIndexes();
@@ -122,7 +122,7 @@ class Indexer {
const chainValid = await blocks.$generateBlockDatabase(); const chainValid = await blocks.$generateBlockDatabase();
if (chainValid === false) { if (chainValid === false) {
// Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration // Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration
logger.warn(`The chain of block hash is invalid, re-indexing invalid data in 10 seconds.`); logger.warn(`The chain of block hash is invalid, re-indexing invalid data in 10 seconds.`, logger.tags.mining);
setTimeout(() => this.reindex(), 10000); setTimeout(() => this.reindex(), 10000);
this.indexerRunning = false; this.indexerRunning = false;
return; return;

View File

@@ -69,6 +69,10 @@ class Logger {
this.network = this.getNetwork(); this.network = this.getNetwork();
} }
public updateNetwork(): void {
this.network = this.getNetwork();
}
private addprio(prio): void { private addprio(prio): void {
this[prio] = (function(_this) { this[prio] = (function(_this) {
return function(msg, tag?: string) { return function(msg, tag?: string) {

View File

@@ -153,6 +153,7 @@ export interface BlockExtension {
feeRange: number[]; // fee rate percentiles feeRange: number[]; // fee rate percentiles
reward: number; reward: number;
matchRate: number | null; matchRate: number | null;
similarity?: number;
pool: { pool: {
id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id` id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
name: string; name: string;
@@ -293,7 +294,6 @@ interface RequiredParams {
} }
export interface ILoadingIndicators { [name: string]: number; } export interface ILoadingIndicators { [name: string]: number; }
export interface IConversionRates { [currency: string]: number; }
export interface IBackendInfo { export interface IBackendInfo {
hostname: string; hostname: string;

View File

@@ -330,6 +330,55 @@ class BlocksRepository {
} }
} }
/**
* Get average block health for all blocks for a single pool
*/
public async $getAvgBlockHealthPerPoolId(poolId: number): Promise<number> {
const params: any[] = [];
const query = `
SELECT AVG(blocks_audits.match_rate) AS avg_match_rate
FROM blocks
JOIN blocks_audits ON blocks.height = blocks_audits.height
WHERE blocks.pool_id = ?
`;
params.push(poolId);
try {
const [rows] = await DB.query(query, params);
if (!rows[0] || !rows[0].avg_match_rate) {
return 0;
}
return Math.round(rows[0].avg_match_rate * 100) / 100;
} catch (e) {
logger.err(`Cannot get average block health for pool id ${poolId}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get average block health for all blocks for a single pool
*/
public async $getTotalRewardForPoolId(poolId: number): Promise<number> {
const params: any[] = [];
const query = `
SELECT sum(reward) as total_reward
FROM blocks
WHERE blocks.pool_id = ?
`;
params.push(poolId);
try {
const [rows] = await DB.query(query, params);
if (!rows[0] || !rows[0].total_reward) {
return 0;
}
return rows[0].total_reward;
} catch (e) {
logger.err(`Cannot get total reward for pool id ${poolId}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/** /**
* Get the oldest indexed block * Get the oldest indexed block
*/ */
@@ -748,6 +797,7 @@ class BlocksRepository {
SELECT height SELECT height
FROM compact_cpfp_clusters FROM compact_cpfp_clusters
WHERE height <= ? AND height >= ? WHERE height <= ? AND height >= ?
GROUP BY height
ORDER BY height DESC; ORDER BY height DESC;
`, [currentBlockHeight, minHeight]); `, [currentBlockHeight, minHeight]);

View File

@@ -20,9 +20,9 @@ class DifficultyAdjustmentsRepository {
await DB.query(query, params); await DB.query(query, params);
} catch (e: any) { } catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Cannot save difficulty adjustment at block ${adjustment.height}, already indexed, ignoring`); logger.debug(`Cannot save difficulty adjustment at block ${adjustment.height}, already indexed, ignoring`, logger.tags.mining);
} else { } else {
logger.err(`Cannot save difficulty adjustment at block ${adjustment.height}. Reason: ${e instanceof Error ? e.message : e}`); logger.err(`Cannot save difficulty adjustment at block ${adjustment.height}. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
throw e; throw e;
} }
} }
@@ -54,7 +54,7 @@ class DifficultyAdjustmentsRepository {
const [rows] = await DB.query(query); const [rows] = await DB.query(query);
return rows as IndexedDifficultyAdjustment[]; return rows as IndexedDifficultyAdjustment[];
} catch (e) { } catch (e) {
logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@@ -83,7 +83,7 @@ class DifficultyAdjustmentsRepository {
const [rows] = await DB.query(query); const [rows] = await DB.query(query);
return rows as IndexedDifficultyAdjustment[]; return rows as IndexedDifficultyAdjustment[];
} catch (e) { } catch (e) {
logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@@ -93,27 +93,27 @@ class DifficultyAdjustmentsRepository {
const [rows]: any[] = await DB.query(`SELECT height FROM difficulty_adjustments`); const [rows]: any[] = await DB.query(`SELECT height FROM difficulty_adjustments`);
return rows.map(block => block.height); return rows.map(block => block.height);
} catch (e: any) { } catch (e: any) {
logger.err(`Cannot get difficulty adjustment block heights. Reason: ${e instanceof Error ? e.message : e}`); logger.err(`Cannot get difficulty adjustment block heights. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
throw e; throw e;
} }
} }
public async $deleteAdjustementsFromHeight(height: number): Promise<void> { public async $deleteAdjustementsFromHeight(height: number): Promise<void> {
try { try {
logger.info(`Delete newer difficulty adjustments from height ${height} from the database`); logger.info(`Delete newer difficulty adjustments from height ${height} from the database`, logger.tags.mining);
await DB.query(`DELETE FROM difficulty_adjustments WHERE height >= ?`, [height]); await DB.query(`DELETE FROM difficulty_adjustments WHERE height >= ?`, [height]);
} catch (e: any) { } catch (e: any) {
logger.err(`Cannot delete difficulty adjustments from the database. Reason: ${e instanceof Error ? e.message : e}`); logger.err(`Cannot delete difficulty adjustments from the database. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
throw e; throw e;
} }
} }
public async $deleteLastAdjustment(): Promise<void> { public async $deleteLastAdjustment(): Promise<void> {
try { try {
logger.info(`Delete last difficulty adjustment from the database`); logger.info(`Delete last difficulty adjustment from the database`, logger.tags.mining);
await DB.query(`DELETE FROM difficulty_adjustments ORDER BY time LIMIT 1`); await DB.query(`DELETE FROM difficulty_adjustments ORDER BY time LIMIT 1`);
} catch (e: any) { } catch (e: any) {
logger.err(`Cannot delete last difficulty adjustment from the database. Reason: ${e instanceof Error ? e.message : e}`); logger.err(`Cannot delete last difficulty adjustment from the database. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
throw e; throw e;
} }
} }

View File

@@ -25,7 +25,7 @@ class HashratesRepository {
try { try {
await DB.query(query); await DB.query(query);
} catch (e: any) { } catch (e: any) {
logger.err('Cannot save indexed hashrate into db. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot save indexed hashrate into db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@@ -51,7 +51,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query); const [rows]: any[] = await DB.query(query);
return rows; return rows;
} catch (e) { } catch (e) {
logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@@ -78,7 +78,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query); const [rows]: any[] = await DB.query(query);
return rows; return rows;
} catch (e) { } catch (e) {
logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@@ -93,7 +93,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query); const [rows]: any[] = await DB.query(query);
return rows.map(row => row.timestamp); return rows.map(row => row.timestamp);
} catch (e) { } catch (e) {
logger.err('Cannot retreive indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot retreive indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@@ -128,7 +128,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query); const [rows]: any[] = await DB.query(query);
return rows; return rows;
} catch (e) { } catch (e) {
logger.err('Cannot fetch weekly pools hashrate history. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot fetch weekly pools hashrate history. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@@ -158,7 +158,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query, [pool.id]); const [rows]: any[] = await DB.query(query, [pool.id]);
boundaries = rows[0]; boundaries = rows[0];
} catch (e) { } catch (e) {
logger.err('Cannot fetch hashrate start/end timestamps for this pool. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot fetch hashrate start/end timestamps for this pool. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
} }
// Get hashrates entries between boundaries // Get hashrates entries between boundaries
@@ -173,7 +173,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query, [boundaries.firstTimestamp, boundaries.lastTimestamp, pool.id]); const [rows]: any[] = await DB.query(query, [boundaries.firstTimestamp, boundaries.lastTimestamp, pool.id]);
return rows; return rows;
} catch (e) { } catch (e) {
logger.err('Cannot fetch pool hashrate history for this pool. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot fetch pool hashrate history for this pool. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@@ -192,7 +192,7 @@ class HashratesRepository {
} }
return rows[0]['number']; return rows[0]['number'];
} catch (e) { } catch (e) {
logger.err(`Cannot retrieve last indexing run for ${key}. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot retrieve last indexing run for ${key}. Reason: ` + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@@ -201,7 +201,7 @@ class HashratesRepository {
* Delete most recent data points for re-indexing * Delete most recent data points for re-indexing
*/ */
public async $deleteLastEntries() { public async $deleteLastEntries() {
logger.info(`Delete latest hashrates data points from the database`); logger.info(`Delete latest hashrates data points from the database`, logger.tags.mining);
try { try {
const [rows]: any[] = await DB.query(`SELECT MAX(hashrate_timestamp) as timestamp FROM hashrates GROUP BY type`); const [rows]: any[] = await DB.query(`SELECT MAX(hashrate_timestamp) as timestamp FROM hashrates GROUP BY type`);
@@ -212,7 +212,7 @@ class HashratesRepository {
mining.lastHashrateIndexingDate = null; mining.lastHashrateIndexingDate = null;
mining.lastWeeklyHashrateIndexingDate = null; mining.lastWeeklyHashrateIndexingDate = null;
} catch (e) { } catch (e) {
logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
} }
} }
@@ -228,7 +228,7 @@ class HashratesRepository {
mining.lastHashrateIndexingDate = null; mining.lastHashrateIndexingDate = null;
mining.lastWeeklyHashrateIndexingDate = null; mining.lastWeeklyHashrateIndexingDate = null;
} catch (e) { } catch (e) {
logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
} }
} }
} }

View File

@@ -1,6 +1,5 @@
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import { IConversionRates } from '../mempool.interfaces';
import priceUpdater from '../tasks/price-updater'; import priceUpdater from '../tasks/price-updater';
export interface ApiPrice { export interface ApiPrice {
@@ -13,6 +12,16 @@ export interface ApiPrice {
AUD: number, AUD: number,
JPY: number, JPY: number,
} }
const ApiPriceFields = `
UNIX_TIMESTAMP(time) as time,
USD,
EUR,
GBP,
CAD,
CHF,
AUD,
JPY
`;
export interface ExchangeRates { export interface ExchangeRates {
USDEUR: number, USDEUR: number,
@@ -39,7 +48,7 @@ export const MAX_PRICES = {
}; };
class PricesRepository { class PricesRepository {
public async $savePrices(time: number, prices: IConversionRates): Promise<void> { public async $savePrices(time: number, prices: ApiPrice): Promise<void> {
if (prices.USD === -1) { if (prices.USD === -1) {
// Some historical price entries have no USD prices, so we just ignore them to avoid future UX issues // Some historical price entries have no USD prices, so we just ignore them to avoid future UX issues
// As of today there are only 4 (on 2013-09-05, 2013-0909, 2013-09-12 and 2013-09-26) so that's fine // As of today there are only 4 (on 2013-09-05, 2013-0909, 2013-09-12 and 2013-09-26) so that's fine
@@ -60,77 +69,115 @@ class PricesRepository {
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ? )`, VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ? )`,
[time, prices.USD, prices.EUR, prices.GBP, prices.CAD, prices.CHF, prices.AUD, prices.JPY] [time, prices.USD, prices.EUR, prices.GBP, prices.CAD, prices.CHF, prices.AUD, prices.JPY]
); );
} catch (e: any) { } catch (e) {
logger.err(`Cannot save exchange rate into db. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot save exchange rate into db. Reason: ` + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
} }
public async $getOldestPriceTime(): Promise<number> { public async $getOldestPriceTime(): Promise<number> {
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != 0 ORDER BY time LIMIT 1`); const [oldestRow] = await DB.query(`
SELECT UNIX_TIMESTAMP(time) AS time
FROM prices
ORDER BY time
LIMIT 1
`);
return oldestRow[0] ? oldestRow[0].time : 0; return oldestRow[0] ? oldestRow[0].time : 0;
} }
public async $getLatestPriceId(): Promise<number | null> { public async $getLatestPriceId(): Promise<number | null> {
const [oldestRow] = await DB.query(`SELECT id from prices WHERE USD != 0 ORDER BY time DESC LIMIT 1`); const [oldestRow] = await DB.query(`
return oldestRow[0] ? oldestRow[0].id : null; SELECT id
}
public async $getLatestPriceTime(): Promise<number> {
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != 0 ORDER BY time DESC LIMIT 1`);
return oldestRow[0] ? oldestRow[0].time : 0;
}
public async $getPricesTimes(): Promise<number[]> {
const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != 0 ORDER BY time`);
return times.map(time => time.time);
}
public async $getPricesTimesAndId(): Promise<number[]> {
const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time, id, USD from prices ORDER BY time`);
return times;
}
public async $getLatestConversionRates(): Promise<any> {
const [rates]: any[] = await DB.query(`
SELECT USD, EUR, GBP, CAD, CHF, AUD, JPY
FROM prices FROM prices
ORDER BY time DESC ORDER BY time DESC
LIMIT 1` LIMIT 1`
); );
if (!rates || rates.length === 0) { return oldestRow[0] ? oldestRow[0].id : null;
}
public async $getLatestPriceTime(): Promise<number> {
const [oldestRow] = await DB.query(`
SELECT UNIX_TIMESTAMP(time) AS time
FROM prices
ORDER BY time DESC
LIMIT 1`
);
return oldestRow[0] ? oldestRow[0].time : 0;
}
public async $getPricesTimes(): Promise<number[]> {
const [times] = await DB.query(`
SELECT UNIX_TIMESTAMP(time) AS time
FROM prices
WHERE USD != -1
ORDER BY time
`);
if (!Array.isArray(times)) {
return [];
}
return times.map(time => time.time);
}
public async $getPricesTimesAndId(): Promise<{time: number, id: number, USD: number}[]> {
const [times] = await DB.query(`
SELECT
UNIX_TIMESTAMP(time) AS time,
id,
USD
FROM prices
ORDER BY time
`);
return times as {time: number, id: number, USD: number}[];
}
public async $getLatestConversionRates(): Promise<ApiPrice> {
const [rates] = await DB.query(`
SELECT ${ApiPriceFields}
FROM prices
ORDER BY time DESC
LIMIT 1`
);
if (!Array.isArray(rates) || rates.length === 0) {
return priceUpdater.getEmptyPricesObj(); return priceUpdater.getEmptyPricesObj();
} }
return rates[0]; return rates[0] as ApiPrice;
} }
public async $getNearestHistoricalPrice(timestamp: number | undefined): Promise<Conversion | null> { public async $getNearestHistoricalPrice(timestamp: number | undefined): Promise<Conversion | null> {
try { try {
const [rates]: any[] = await DB.query(` const [rates] = await DB.query(`
SELECT *, UNIX_TIMESTAMP(time) AS time SELECT ${ApiPriceFields}
FROM prices FROM prices
WHERE UNIX_TIMESTAMP(time) < ? WHERE UNIX_TIMESTAMP(time) < ?
ORDER BY time DESC ORDER BY time DESC
LIMIT 1`, LIMIT 1`,
[timestamp] [timestamp]
); );
if (!rates) { if (!Array.isArray(rates)) {
throw Error(`Cannot get single historical price from the database`); throw Error(`Cannot get single historical price from the database`);
} }
// Compute fiat exchange rates // Compute fiat exchange rates
const latestPrice = await this.$getLatestConversionRates(); let latestPrice = rates[0] as ApiPrice;
if (latestPrice.USD === -1) {
latestPrice = priceUpdater.getEmptyPricesObj();
}
const computeFx = (usd: number, other: number): number =>
Math.round(Math.max(other, 0) / Math.max(usd, 1) * 100) / 100;
const exchangeRates: ExchangeRates = { const exchangeRates: ExchangeRates = {
USDEUR: Math.round(latestPrice.EUR / latestPrice.USD * 100) / 100, USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
USDGBP: Math.round(latestPrice.GBP / latestPrice.USD * 100) / 100, USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
USDCAD: Math.round(latestPrice.CAD / latestPrice.USD * 100) / 100, USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
USDCHF: Math.round(latestPrice.CHF / latestPrice.USD * 100) / 100, USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
USDAUD: Math.round(latestPrice.AUD / latestPrice.USD * 100) / 100, USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
USDJPY: Math.round(latestPrice.JPY / latestPrice.USD * 100) / 100, USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
}; };
return { return {
prices: rates, prices: rates as ApiPrice[],
exchangeRates: exchangeRates exchangeRates: exchangeRates
}; };
} catch (e) { } catch (e) {
@@ -141,28 +188,35 @@ class PricesRepository {
public async $getHistoricalPrices(): Promise<Conversion | null> { public async $getHistoricalPrices(): Promise<Conversion | null> {
try { try {
const [rates]: any[] = await DB.query(` const [rates] = await DB.query(`
SELECT *, UNIX_TIMESTAMP(time) AS time SELECT ${ApiPriceFields}
FROM prices FROM prices
ORDER BY time DESC ORDER BY time DESC
`); `);
if (!rates) { if (!Array.isArray(rates)) {
throw Error(`Cannot get average historical price from the database`); throw Error(`Cannot get average historical price from the database`);
} }
// Compute fiat exchange rates // Compute fiat exchange rates
const latestPrice: ApiPrice = rates[0]; let latestPrice = rates[0] as ApiPrice;
if (latestPrice.USD === -1) {
latestPrice = priceUpdater.getEmptyPricesObj();
}
const computeFx = (usd: number, other: number): number =>
Math.round(Math.max(other, 0) / Math.max(usd, 1) * 100) / 100;
const exchangeRates: ExchangeRates = { const exchangeRates: ExchangeRates = {
USDEUR: Math.round(latestPrice.EUR / latestPrice.USD * 100) / 100, USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
USDGBP: Math.round(latestPrice.GBP / latestPrice.USD * 100) / 100, USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
USDCAD: Math.round(latestPrice.CAD / latestPrice.USD * 100) / 100, USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
USDCHF: Math.round(latestPrice.CHF / latestPrice.USD * 100) / 100, USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
USDAUD: Math.round(latestPrice.AUD / latestPrice.USD * 100) / 100, USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
USDJPY: Math.round(latestPrice.JPY / latestPrice.USD * 100) / 100, USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
}; };
return { return {
prices: rates, prices: rates as ApiPrice[],
exchangeRates: exchangeRates exchangeRates: exchangeRates
}; };
} catch (e) { } catch (e) {

View File

@@ -22,12 +22,15 @@ class LightningStatsUpdater {
* Update the latest entry for each node every config.LIGHTNING.STATS_REFRESH_INTERVAL seconds * Update the latest entry for each node every config.LIGHTNING.STATS_REFRESH_INTERVAL seconds
*/ */
private async $logStatsDaily(): Promise<void> { private async $logStatsDaily(): Promise<void> {
const date = new Date(); try {
Common.setDateMidnight(date); const date = new Date();
const networkGraph = await lightningApi.$getNetworkGraph(); Common.setDateMidnight(date);
await LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph); const networkGraph = await lightningApi.$getNetworkGraph();
await LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph);
logger.debug(`Updated latest network stats`, logger.tags.ln); logger.debug(`Updated latest network stats`, logger.tags.ln);
} catch (e) {
logger.err(`Exception in $logStatsDaily. Reason: ${(e instanceof Error ? e.message : e)}`);
}
} }
} }

View File

@@ -411,7 +411,7 @@ class LightningStatsImporter {
} }
if (totalProcessed > 0) { if (totalProcessed > 0) {
logger.notice(`Lightning network stats historical import completed`, logger.tags.ln); logger.info(`Lightning network stats historical import completed`, logger.tags.ln);
} }
} catch (e) { } catch (e) {
logger.err(`Lightning network stats historical failed. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.ln); logger.err(`Lightning network stats historical failed. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.ln);

View File

@@ -12,12 +12,14 @@ import * as https from 'https';
*/ */
class PoolsUpdater { class PoolsUpdater {
lastRun: number = 0; lastRun: number = 0;
currentSha: string | undefined = undefined; currentSha: string | null = null;
poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL; poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL;
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL; treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL;
public async updatePoolsJson(): Promise<void> { public async updatePoolsJson(): Promise<void> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false ||
config.MEMPOOL.ENABLED === false
) {
return; return;
} }
@@ -33,7 +35,7 @@ class PoolsUpdater {
try { try {
const githubSha = await this.fetchPoolsSha(); // Fetch pools-v2.json sha from github const githubSha = await this.fetchPoolsSha(); // Fetch pools-v2.json sha from github
if (githubSha === undefined) { if (githubSha === null) {
return; return;
} }
@@ -42,12 +44,12 @@ class PoolsUpdater {
} }
logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`); logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`);
if (this.currentSha !== undefined && this.currentSha === githubSha) { if (this.currentSha !== null && this.currentSha === githubSha) {
return; return;
} }
// See backend README for more details about the mining pools update process // See backend README for more details about the mining pools update process
if (this.currentSha !== undefined && // If we don't have any mining pool, download it at least once if (this.currentSha !== null && // If we don't have any mining pool, download it at least once
config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING !== true && // Automatic pools update is disabled config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING !== true && // Automatic pools update is disabled
!process.env.npm_config_update_pools // We're not manually updating mining pool !process.env.npm_config_update_pools // We're not manually updating mining pool
) { ) {
@@ -57,7 +59,7 @@ class PoolsUpdater {
} }
const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet'; const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet';
if (this.currentSha === undefined) { if (this.currentSha === null) {
logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining); logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining);
} else { } else {
logger.warn(`pools-v2.json is outdated, fetch latest from ${this.poolsUrl} over ${network}`, logger.tags.mining); logger.warn(`pools-v2.json is outdated, fetch latest from ${this.poolsUrl} over ${network}`, logger.tags.mining);
@@ -82,7 +84,7 @@ class PoolsUpdater {
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining); logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
await DB.query('ROLLBACK;'); await DB.query('ROLLBACK;');
} }
logger.notice('PoolsUpdater completed'); logger.info('PoolsUpdater completed');
} catch (e) { } catch (e) {
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
@@ -108,20 +110,20 @@ class PoolsUpdater {
/** /**
* Fetch our latest pools-v2.json sha from the db * Fetch our latest pools-v2.json sha from the db
*/ */
private async getShaFromDb(): Promise<string | undefined> { private async getShaFromDb(): Promise<string | null> {
try { try {
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"'); const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
return (rows.length > 0 ? rows[0].string : undefined); return (rows.length > 0 ? rows[0].string : null);
} catch (e) { } catch (e) {
logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
return undefined; return null;
} }
} }
/** /**
* Fetch our latest pools-v2.json sha from github * Fetch our latest pools-v2.json sha from github
*/ */
private async fetchPoolsSha(): Promise<string | undefined> { private async fetchPoolsSha(): Promise<string | null> {
const response = await this.query(this.treeUrl); const response = await this.query(this.treeUrl);
if (response !== undefined) { if (response !== undefined) {
@@ -133,7 +135,7 @@ class PoolsUpdater {
} }
logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, logger.tags.mining); logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, logger.tags.mining);
return undefined; return null;
} }
/** /**

View File

@@ -8,9 +8,6 @@ class BitfinexApi implements PriceFeed {
public url: string = 'https://api.bitfinex.com/v1/pubticker/BTC'; public url: string = 'https://api.bitfinex.com/v1/pubticker/BTC';
public urlHist: string = 'https://api-pub.bitfinex.com/v2/candles/trade:{GRANULARITY}:tBTC{CURRENCY}/hist'; public urlHist: string = 'https://api-pub.bitfinex.com/v2/candles/trade:{GRANULARITY}:tBTC{CURRENCY}/hist';
constructor() {
}
public async $fetchPrice(currency): Promise<number> { public async $fetchPrice(currency): Promise<number> {
const response = await query(this.url + currency); const response = await query(this.url + currency);
if (response && response['last_price']) { if (response && response['last_price']) {

View File

@@ -98,7 +98,7 @@ class KrakenApi implements PriceFeed {
} }
if (Object.keys(priceHistory).length > 0) { if (Object.keys(priceHistory).length > 0) {
logger.notice(`Inserted ${Object.keys(priceHistory).length} Kraken EUR, USD, GBP, JPY, CAD, CHF and AUD weekly price history into db`, logger.tags.mining); logger.info(`Inserted ${Object.keys(priceHistory).length} Kraken EUR, USD, GBP, JPY, CAD, CHF and AUD weekly price history into db`, logger.tags.mining);
} }
} }
} }

View File

@@ -2,8 +2,7 @@ import * as fs from 'fs';
import path from 'path'; import path from 'path';
import config from '../config'; import config from '../config';
import logger from '../logger'; import logger from '../logger';
import { IConversionRates } from '../mempool.interfaces'; import PricesRepository, { ApiPrice, MAX_PRICES } from '../repositories/PricesRepository';
import PricesRepository, { MAX_PRICES } from '../repositories/PricesRepository';
import BitfinexApi from './price-feeds/bitfinex-api'; import BitfinexApi from './price-feeds/bitfinex-api';
import BitflyerApi from './price-feeds/bitflyer-api'; import BitflyerApi from './price-feeds/bitflyer-api';
import CoinbaseApi from './price-feeds/coinbase-api'; import CoinbaseApi from './price-feeds/coinbase-api';
@@ -21,18 +20,18 @@ export interface PriceFeed {
} }
export interface PriceHistory { export interface PriceHistory {
[timestamp: number]: IConversionRates; [timestamp: number]: ApiPrice;
} }
class PriceUpdater { class PriceUpdater {
public historyInserted = false; public historyInserted = false;
lastRun = 0; private lastRun = 0;
lastHistoricalRun = 0; private lastHistoricalRun = 0;
running = false; private running = false;
feeds: PriceFeed[] = []; private feeds: PriceFeed[] = [];
currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY']; private currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'];
latestPrices: IConversionRates; private latestPrices: ApiPrice;
private ratesChangedCallback: ((rates: IConversionRates) => void) | undefined; private ratesChangedCallback: ((rates: ApiPrice) => void) | undefined;
constructor() { constructor() {
this.latestPrices = this.getEmptyPricesObj(); this.latestPrices = this.getEmptyPricesObj();
@@ -44,8 +43,13 @@ class PriceUpdater {
this.feeds.push(new GeminiApi()); this.feeds.push(new GeminiApi());
} }
public getEmptyPricesObj(): IConversionRates { public getLatestPrices(): ApiPrice {
return this.latestPrices;
}
public getEmptyPricesObj(): ApiPrice {
return { return {
time: 0,
USD: -1, USD: -1,
EUR: -1, EUR: -1,
GBP: -1, GBP: -1,
@@ -56,7 +60,7 @@ class PriceUpdater {
}; };
} }
public setRatesChangedCallback(fn: (rates: IConversionRates) => void) { public setRatesChangedCallback(fn: (rates: ApiPrice) => void): void {
this.ratesChangedCallback = fn; this.ratesChangedCallback = fn;
} }
@@ -69,6 +73,11 @@ class PriceUpdater {
} }
public async $run(): Promise<void> { public async $run(): Promise<void> {
if (config.MEMPOOL.NETWORK === 'signet' || config.MEMPOOL.NETWORK === 'testnet') {
// Coins have no value on testnet/signet, so we want to always show 0
return;
}
if (this.running === true) { if (this.running === true) {
return; return;
} }
@@ -84,7 +93,7 @@ class PriceUpdater {
if (this.historyInserted === false && config.DATABASE.ENABLED === true) { if (this.historyInserted === false && config.DATABASE.ENABLED === true) {
await this.$insertHistoricalPrices(); await this.$insertHistoricalPrices();
} }
} catch (e) { } catch (e: any) {
logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining); logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
} }
@@ -156,6 +165,10 @@ class PriceUpdater {
} }
this.lastRun = new Date().getTime() / 1000; this.lastRun = new Date().getTime() / 1000;
if (this.latestPrices.USD === -1) {
this.latestPrices = await PricesRepository.$getLatestConversionRates();
}
} }
/** /**
@@ -224,7 +237,7 @@ class PriceUpdater {
// Group them by timestamp and currency, for example // Group them by timestamp and currency, for example
// grouped[123456789]['USD'] = [1, 2, 3, 4]; // grouped[123456789]['USD'] = [1, 2, 3, 4];
const grouped: any = {}; const grouped = {};
for (const historicalEntry of historicalPrices) { for (const historicalEntry of historicalPrices) {
for (const time in historicalEntry) { for (const time in historicalEntry) {
if (existingPriceTimes.includes(parseInt(time, 10))) { if (existingPriceTimes.includes(parseInt(time, 10))) {
@@ -249,7 +262,7 @@ class PriceUpdater {
// Average prices and insert everything into the db // Average prices and insert everything into the db
let totalInserted = 0; let totalInserted = 0;
for (const time in grouped) { for (const time in grouped) {
const prices: IConversionRates = this.getEmptyPricesObj(); const prices: ApiPrice = this.getEmptyPricesObj();
for (const currency in grouped[time]) { for (const currency in grouped[time]) {
if (grouped[time][currency].length === 0) { if (grouped[time][currency].length === 0) {
continue; continue;

View File

@@ -0,0 +1,29 @@
const byteUnits = ['B', 'kB', 'MB', 'GB', 'TB'];
export function getBytesUnit(bytes: number): string {
if (isNaN(bytes) || !isFinite(bytes)) {
return 'B';
}
let unitIndex = 0;
while (unitIndex < byteUnits.length && bytes > 1024) {
unitIndex++;
bytes /= 1024;
}
return byteUnits[unitIndex];
}
export function formatBytes(bytes: number, toUnit: string, skipUnit = false): string {
if (isNaN(bytes) || !isFinite(bytes)) {
return `${bytes}`;
}
let unitIndex = 0;
while (unitIndex < byteUnits.length && (toUnit && byteUnits[unitIndex] !== toUnit || (!toUnit && bytes > 1024))) {
unitIndex++;
bytes /= 1024;
}
return `${bytes.toFixed(2)}${skipUnit ? '' : ' ' + byteUnits[unitIndex]}`;
}

View File

@@ -34,6 +34,7 @@ If you want to use different credentials, specify them in the `docker-compose.ym
CORE_RPC_PORT: "8332" CORE_RPC_PORT: "8332"
CORE_RPC_USERNAME: "customuser" CORE_RPC_USERNAME: "customuser"
CORE_RPC_PASSWORD: "custompassword" CORE_RPC_PASSWORD: "custompassword"
CORE_RPC_TIMEOUT: "60000"
``` ```
The IP address in the example above refers to Docker's default gateway IP address so that the container can hit the `bitcoind` instance running on the host machine. If your setup is different, update it accordingly. The IP address in the example above refers to Docker's default gateway IP address so that the container can hit the `bitcoind` instance running on the host machine. If your setup is different, update it accordingly.
@@ -112,6 +113,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over
"ADVANCED_GBT_MEMPOOL": false, "ADVANCED_GBT_MEMPOOL": false,
"CPFP_INDEXING": false, "CPFP_INDEXING": false,
"MAX_BLOCKS_BULK_QUERY": 0, "MAX_BLOCKS_BULK_QUERY": 0,
"DISK_CACHE_BLOCK_INTERVAL": 6
}, },
``` ```
@@ -143,6 +145,7 @@ Corresponding `docker-compose.yml` overrides:
MEMPOOL_ADVANCED_GBT_MEMPOOL: "" MEMPOOL_ADVANCED_GBT_MEMPOOL: ""
MEMPOOL_CPFP_INDEXING: "" MEMPOOL_CPFP_INDEXING: ""
MAX_BLOCKS_BULK_QUERY: "" MAX_BLOCKS_BULK_QUERY: ""
DISK_CACHE_BLOCK_INTERVAL: ""
... ...
``` ```
@@ -158,7 +161,8 @@ Corresponding `docker-compose.yml` overrides:
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 8332, "PORT": 8332,
"USERNAME": "mempool", "USERNAME": "mempool",
"PASSWORD": "mempool" "PASSWORD": "mempool",
"TIMEOUT": 60000
}, },
``` ```
@@ -170,6 +174,7 @@ Corresponding `docker-compose.yml` overrides:
CORE_RPC_PORT: "" CORE_RPC_PORT: ""
CORE_RPC_USERNAME: "" CORE_RPC_USERNAME: ""
CORE_RPC_PASSWORD: "" CORE_RPC_PASSWORD: ""
CORE_RPC_TIMEOUT: 60000
... ...
``` ```
@@ -219,7 +224,8 @@ Corresponding `docker-compose.yml` overrides:
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 8332, "PORT": 8332,
"USERNAME": "mempool", "USERNAME": "mempool",
"PASSWORD": "mempool" "PASSWORD": "mempool",
"TIMEOUT": 60000
}, },
``` ```
@@ -231,6 +237,7 @@ Corresponding `docker-compose.yml` overrides:
SECOND_CORE_RPC_PORT: "" SECOND_CORE_RPC_PORT: ""
SECOND_CORE_RPC_USERNAME: "" SECOND_CORE_RPC_USERNAME: ""
SECOND_CORE_RPC_PASSWORD: "" SECOND_CORE_RPC_PASSWORD: ""
SECOND_CORE_RPC_TIMEOUT: ""
... ...
``` ```
@@ -403,6 +410,7 @@ Corresponding `docker-compose.yml` overrides:
"TLS_CERT_PATH": "" "TLS_CERT_PATH": ""
"MACAROON_PATH": "" "MACAROON_PATH": ""
"REST_API_URL": "https://localhost:8080" "REST_API_URL": "https://localhost:8080"
"TIMEOUT": 10000
} }
``` ```
@@ -413,6 +421,7 @@ Corresponding `docker-compose.yml` overrides:
LND_TLS_CERT_PATH: "" LND_TLS_CERT_PATH: ""
LND_MACAROON_PATH: "" LND_MACAROON_PATH: ""
LND_REST_API_URL: "https://localhost:8080" LND_REST_API_URL: "https://localhost:8080"
LND_TIMEOUT: 10000
... ...
``` ```
@@ -432,3 +441,26 @@ Corresponding `docker-compose.yml` overrides:
CLIGHTNING_SOCKET: "" CLIGHTNING_SOCKET: ""
... ...
``` ```
<br/>
`mempool-config.json`:
```json
"MAXMIND": {
"ENABLED": true,
"GEOLITE2_CITY": "/usr/local/share/GeoIP/GeoLite2-City.mmdb",
"GEOLITE2_ASN": "/usr/local/share/GeoIP/GeoLite2-ASN.mmdb",
"GEOIP2_ISP": "/usr/local/share/GeoIP/GeoIP2-ISP.mmdb"
}
```
Corresponding `docker-compose.yml` overrides:
```yaml
api:
environment:
MAXMIND_ENABLED: true,
MAXMIND_GEOLITE2_CITY: "/backend/GeoIP/GeoLite2-City.mmdb",
MAXMIND_GEOLITE2_ASN": "/backend/GeoIP/GeoLite2-ASN.mmdb",
MAXMIND_GEOIP2_ISP": "/backend/GeoIP/GeoIP2-ISP.mmdb"
...
```

View File

@@ -17,6 +17,7 @@ WORKDIR /backend
RUN chown 1000:1000 ./ RUN chown 1000:1000 ./
COPY --from=builder --chown=1000:1000 /build/package ./package/ COPY --from=builder --chown=1000:1000 /build/package ./package/
COPY --from=builder --chown=1000:1000 /build/GeoIP ./GeoIP/
COPY --from=builder --chown=1000:1000 /build/mempool-config.json /build/start.sh /build/wait-for-it.sh ./ COPY --from=builder --chown=1000:1000 /build/mempool-config.json /build/start.sh /build/wait-for-it.sh ./
USER 1000 USER 1000

View File

@@ -26,13 +26,15 @@
"ADVANCED_GBT_AUDIT": __MEMPOOL_ADVANCED_GBT_AUDIT__, "ADVANCED_GBT_AUDIT": __MEMPOOL_ADVANCED_GBT_AUDIT__,
"ADVANCED_GBT_MEMPOOL": __MEMPOOL_ADVANCED_GBT_MEMPOOL__, "ADVANCED_GBT_MEMPOOL": __MEMPOOL_ADVANCED_GBT_MEMPOOL__,
"CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__, "CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__,
"MAX_BLOCKS_BULK_QUERY": __MEMPOOL__MAX_BLOCKS_BULK_QUERY__ "MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__,
"DISK_CACHE_BLOCK_INTERVAL": __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "__CORE_RPC_HOST__", "HOST": "__CORE_RPC_HOST__",
"PORT": __CORE_RPC_PORT__, "PORT": __CORE_RPC_PORT__,
"USERNAME": "__CORE_RPC_USERNAME__", "USERNAME": "__CORE_RPC_USERNAME__",
"PASSWORD": "__CORE_RPC_PASSWORD__" "PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": __CORE_RPC_TIMEOUT__
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "__ELECTRUM_HOST__", "HOST": "__ELECTRUM_HOST__",
@@ -46,7 +48,8 @@
"HOST": "__SECOND_CORE_RPC_HOST__", "HOST": "__SECOND_CORE_RPC_HOST__",
"PORT": __SECOND_CORE_RPC_PORT__, "PORT": __SECOND_CORE_RPC_PORT__,
"USERNAME": "__SECOND_CORE_RPC_USERNAME__", "USERNAME": "__SECOND_CORE_RPC_USERNAME__",
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__" "PASSWORD": "__SECOND_CORE_RPC_PASSWORD__",
"TIMEOUT": __SECOND_CORE_RPC_TIMEOUT__
}, },
"DATABASE": { "DATABASE": {
"ENABLED": __DATABASE_ENABLED__, "ENABLED": __DATABASE_ENABLED__,
@@ -83,7 +86,8 @@
"LND": { "LND": {
"TLS_CERT_PATH": "__LND_TLS_CERT_PATH__", "TLS_CERT_PATH": "__LND_TLS_CERT_PATH__",
"MACAROON_PATH": "__LND_MACAROON_PATH__", "MACAROON_PATH": "__LND_MACAROON_PATH__",
"REST_API_URL": "__LND_REST_API_URL__" "REST_API_URL": "__LND_REST_API_URL__",
"TIMEOUT": "__LND_TIMEOUT__"
}, },
"CLIGHTNING": { "CLIGHTNING": {
"SOCKET": "__CLIGHTNING_SOCKET__" "SOCKET": "__CLIGHTNING_SOCKET__"
@@ -107,5 +111,11 @@
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__", "LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__",
"BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__", "BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__",
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__" "BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
},
"MAXMIND": {
"ENABLED": __MAXMIND_ENABLED__,
"GEOLITE2_CITY": "__MAXMIND_GEOLITE2_CITY__",
"GEOLITE2_ASN": "__MAXMIND_GEOLITE2_ASN__",
"GEOIP2_ISP": "__MAXMIND_GEOIP2_ISP__"
} }
} }

View File

@@ -22,7 +22,6 @@ __MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1}
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0} __MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool} __MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info} __MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
__MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=false}
__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false} __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false}
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json} __MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json}
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master} __MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
@@ -31,12 +30,14 @@ __MEMPOOL_ADVANCED_GBT_AUDIT__=${MEMPOOL_ADVANCED_GBT_AUDIT:=false}
__MEMPOOL_ADVANCED_GBT_MEMPOOL__=${MEMPOOL_ADVANCED_GBT_MEMPOOL:=false} __MEMPOOL_ADVANCED_GBT_MEMPOOL__=${MEMPOOL_ADVANCED_GBT_MEMPOOL:=false}
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false} __MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0} __MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6}
# CORE_RPC # CORE_RPC
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1} __CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
__CORE_RPC_PORT__=${CORE_RPC_PORT:=8332} __CORE_RPC_PORT__=${CORE_RPC_PORT:=8332}
__CORE_RPC_USERNAME__=${CORE_RPC_USERNAME:=mempool} __CORE_RPC_USERNAME__=${CORE_RPC_USERNAME:=mempool}
__CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool} __CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool}
__CORE_RPC_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000}
# ELECTRUM # ELECTRUM
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1} __ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1}
@@ -51,6 +52,7 @@ __SECOND_CORE_RPC_HOST__=${SECOND_CORE_RPC_HOST:=127.0.0.1}
__SECOND_CORE_RPC_PORT__=${SECOND_CORE_RPC_PORT:=8332} __SECOND_CORE_RPC_PORT__=${SECOND_CORE_RPC_PORT:=8332}
__SECOND_CORE_RPC_USERNAME__=${SECOND_CORE_RPC_USERNAME:=mempool} __SECOND_CORE_RPC_USERNAME__=${SECOND_CORE_RPC_USERNAME:=mempool}
__SECOND_CORE_RPC_PASSWORD__=${SECOND_CORE_RPC_PASSWORD:=mempool} __SECOND_CORE_RPC_PASSWORD__=${SECOND_CORE_RPC_PASSWORD:=mempool}
__SECOND_CORE_RPC_TIMEOUT__=${SECOND_CORE_RPC_TIMEOUT:=60000}
# DATABASE # DATABASE
__DATABASE_ENABLED__=${DATABASE_ENABLED:=true} __DATABASE_ENABLED__=${DATABASE_ENABLED:=true}
@@ -108,10 +110,18 @@ __LIGHTNING_LOGGER_UPDATE_INTERVAL__=${LIGHTNING_LOGGER_UPDATE_INTERVAL:=30}
__LND_TLS_CERT_PATH__=${LND_TLS_CERT_PATH:=""} __LND_TLS_CERT_PATH__=${LND_TLS_CERT_PATH:=""}
__LND_MACAROON_PATH__=${LND_MACAROON_PATH:=""} __LND_MACAROON_PATH__=${LND_MACAROON_PATH:=""}
__LND_REST_API_URL__=${LND_REST_API_URL:="https://localhost:8080"} __LND_REST_API_URL__=${LND_REST_API_URL:="https://localhost:8080"}
__LND_TIMEOUT__=${LND_TIMEOUT:=10000}
# CLN # CLN
__CLIGHTNING_SOCKET__=${CLIGHTNING_SOCKET:=""} __CLIGHTNING_SOCKET__=${CLIGHTNING_SOCKET:=""}
# MAXMIND
__MAXMIND_ENABLED__=${MAXMIND_ENABLED:=true}
__MAXMIND_GEOLITE2_CITY__=${MAXMIND_GEOLITE2_CITY:="/backend/GeoIP/GeoLite2-City.mmdb"}
__MAXMIND_GEOLITE2_ASN__=${MAXMIND_GEOLITE2_ASN:="/backend/GeoIP/GeoLite2-ASN.mmdb"}
__MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""}
mkdir -p "${__MEMPOOL_CACHE_DIR__}" mkdir -p "${__MEMPOOL_CACHE_DIR__}"
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
@@ -135,7 +145,6 @@ sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!g" me
sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json
sed -i "s/__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__/${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}/g" mempool-config.json sed -i "s/__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__/${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}/g" mempool-config.json
sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json
sed -i "s/__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__/${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}/g" mempool-config.json sed -i "s/__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__/${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}/g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
@@ -144,11 +153,13 @@ sed -i "s!__MEMPOOL_ADVANCED_GBT_MEMPOOL__!${__MEMPOOL_ADVANCED_GBT_MEMPOOL__}!g
sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" mempool-config.json sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__}!g" mempool-config.json
sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json
sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json 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_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_PASSWORD__/${__CORE_RPC_PASSWORD__}/g" mempool-config.json
sed -i "s/__CORE_RPC_TIMEOUT__/${__CORE_RPC_TIMEOUT__}/g" mempool-config.json
sed -i "s/__ELECTRUM_HOST__/${__ELECTRUM_HOST__}/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 sed -i "s/__ELECTRUM_PORT__/${__ELECTRUM_PORT__}/g" mempool-config.json
@@ -160,6 +171,7 @@ sed -i "s/__SECOND_CORE_RPC_HOST__/${__SECOND_CORE_RPC_HOST__}/g" mempool-config
sed -i "s/__SECOND_CORE_RPC_PORT__/${__SECOND_CORE_RPC_PORT__}/g" mempool-config.json sed -i "s/__SECOND_CORE_RPC_PORT__/${__SECOND_CORE_RPC_PORT__}/g" mempool-config.json
sed -i "s/__SECOND_CORE_RPC_USERNAME__/${__SECOND_CORE_RPC_USERNAME__}/g" mempool-config.json 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_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/__DATABASE_ENABLED__/${__DATABASE_ENABLED__}/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 sed -i "s/__DATABASE_HOST__/${__DATABASE_HOST__}/g" mempool-config.json
@@ -211,8 +223,16 @@ sed -i "s!__LIGHTNING_LOGGER_UPDATE_INTERVAL__!${__LIGHTNING_LOGGER_UPDATE_INTER
sed -i "s!__LND_TLS_CERT_PATH__!${__LND_TLS_CERT_PATH__}!g" mempool-config.json sed -i "s!__LND_TLS_CERT_PATH__!${__LND_TLS_CERT_PATH__}!g" mempool-config.json
sed -i "s!__LND_MACAROON_PATH__!${__LND_MACAROON_PATH__}!g" mempool-config.json sed -i "s!__LND_MACAROON_PATH__!${__LND_MACAROON_PATH__}!g" mempool-config.json
sed -i "s!__LND_REST_API_URL__!${__LND_REST_API_URL__}!g" mempool-config.json sed -i "s!__LND_REST_API_URL__!${__LND_REST_API_URL__}!g" mempool-config.json
sed -i "s!__LND_TIMEOUT__!${__LND_TIMEOUT__}!g" mempool-config.json
# CLN # CLN
sed -i "s!__CLIGHTNING_SOCKET__!${__CLIGHTNING_SOCKET__}!g" mempool-config.json sed -i "s!__CLIGHTNING_SOCKET__!${__CLIGHTNING_SOCKET__}!g" mempool-config.json
# MAXMIND
sed -i "s!__MAXMIND_ENABLED__!${__MAXMIND_ENABLED__}!g" mempool-config.json
sed -i "s!__MAXMIND_GEOLITE2_CITY__!${__MAXMIND_GEOLITE2_CITY__}!g" mempool-config.json
sed -i "s!__MAXMIND_GEOLITE2_ASN__!${__MAXMIND_GEOLITE2_ASN__}!g" mempool-config.json
sed -i "s!__MAXMIND_GEOIP2_ISP__!${__MAXMIND_GEOIP2_ISP__}!g" mempool-config.json
node /backend/package/index.js node /backend/package/index.js

View File

@@ -10,6 +10,10 @@ cp /etc/nginx/nginx.conf /patch/nginx.conf
sed -i "s/__MEMPOOL_FRONTEND_HTTP_PORT__/${__MEMPOOL_FRONTEND_HTTP_PORT__}/g" /patch/nginx.conf sed -i "s/__MEMPOOL_FRONTEND_HTTP_PORT__/${__MEMPOOL_FRONTEND_HTTP_PORT__}/g" /patch/nginx.conf
cat /patch/nginx.conf > /etc/nginx/nginx.conf cat /patch/nginx.conf > /etc/nginx/nginx.conf
if [ "${LIGHTNING_DETECTED_PORT}" = "9735" ];then
export LIGHTNING=true
fi
# Runtime overrides - read env vars defined in docker compose # Runtime overrides - read env vars defined in docker compose
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false} __TESTNET_ENABLED__=${TESTNET_ENABLED:=false}

View File

@@ -3,6 +3,11 @@
#backend #backend
cp ./docker/backend/* ./backend/ cp ./docker/backend/* ./backend/
#geoip-data
mkdir -p ./backend/GeoIP/
wget -O ./backend/GeoIP/GeoLite2-City.mmdb https://raw.githubusercontent.com/mempool/geoip-data/master/GeoLite2-City.mmdb
wget -O ./backend/GeoIP/GeoLite2-ASN.mmdb https://raw.githubusercontent.com/mempool/geoip-data/master/GeoLite2-ASN.mmdb
#frontend #frontend
localhostIP="127.0.0.1" localhostIP="127.0.0.1"
cp ./docker/frontend/* ./frontend cp ./docker/frontend/* ./frontend

3
frontend/.gitignore vendored
View File

@@ -54,7 +54,8 @@ src/resources/assets-testnet.json
src/resources/assets-testnet.minimal.json src/resources/assets-testnet.minimal.json
src/resources/pools.json src/resources/pools.json
src/resources/mining-pools/* src/resources/mining-pools/*
src/resources/*.mp4 src/resources/**/*.mp4
src/resources/**/*.vtt
# environment config # environment config
mempool-frontend-config.json mempool-frontend-config.json

View File

@@ -111,7 +111,7 @@ https://www.transifex.com/mempool/mempool/dashboard/
* Spanish @maxhodler @bisqes * Spanish @maxhodler @bisqes
* Persian @techmix * Persian @techmix
* French @Bayernatoor * French @Bayernatoor
* Korean @kcalvinalvinn * Korean @kcalvinalvinn @sogoagain
* Italian @HodlBits * Italian @HodlBits
* Hebrew @rapidlab309 * Hebrew @rapidlab309
* Georgian @wyd_idk * Georgian @wyd_idk

View File

@@ -38,6 +38,10 @@
"translation": "src/locale/messages.de.xlf", "translation": "src/locale/messages.de.xlf",
"baseHref": "/de/" "baseHref": "/de/"
}, },
"da": {
"translation": "src/locale/messages.da.xlf",
"baseHref": "/da/"
},
"es": { "es": {
"translation": "src/locale/messages.es.xlf", "translation": "src/locale/messages.es.xlf",
"baseHref": "/es/" "baseHref": "/es/"

View File

@@ -158,10 +158,10 @@ describe('Liquid', () => {
it('show empty unblinded TX', () => { it('show empty unblinded TX', () => {
cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=`); cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', ''); cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', 'Confidential'); cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', ''); cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', ''); cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', 'Confidential'); cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', 'Confidential'); cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', 'Confidential');
}); });
@@ -169,8 +169,8 @@ describe('Liquid', () => {
it('show invalid unblinded TX hex', () => { it('show invalid unblinded TX hex', () => {
cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=123`); cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=123`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr').should('have.class', ''); cy.get('.table-tx-vin tr').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vout tr').should('have.class', ''); cy.get('.table-tx-vout tr').should('have.class', 'ng-star-inserted');
cy.get('.error-unblinded').contains('Error: Invalid blinding data (invalid hex)'); cy.get('.error-unblinded').contains('Error: Invalid blinding data (invalid hex)');
}); });

View File

@@ -109,10 +109,10 @@ describe('Liquid Testnet', () => {
it('show empty unblinded TX', () => { it('show empty unblinded TX', () => {
cy.visit(`${basePath}/tx/c3d908ab77891e4c569b0df71aae90f4720b157019ebb20db176f4f9c4d626b8#blinded=`); cy.visit(`${basePath}/tx/c3d908ab77891e4c569b0df71aae90f4720b157019ebb20db176f4f9c4d626b8#blinded=`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', ''); cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', 'Confidential'); cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', ''); cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', ''); cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', 'Confidential'); cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', 'Confidential'); cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', 'Confidential');
}); });
@@ -120,8 +120,8 @@ describe('Liquid Testnet', () => {
it('show invalid unblinded TX hex', () => { it('show invalid unblinded TX hex', () => {
cy.visit(`${basePath}/tx/2477f220eef1d03f8ffa4a2861c275d155c3562adf0d79523aeeb0c59ee611ba#blinded=5000`); cy.visit(`${basePath}/tx/2477f220eef1d03f8ffa4a2861c275d155c3562adf0d79523aeeb0c59ee611ba#blinded=5000`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr').should('have.class', ''); cy.get('.table-tx-vin tr').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vout tr').should('have.class', ''); cy.get('.table-tx-vout tr').should('have.class', 'ng-star-inserted');
cy.get('.error-unblinded').contains('Error: Invalid blinding data (invalid hex)'); cy.get('.error-unblinded').contains('Error: Invalid blinding data (invalid hex)');
}); });

View File

@@ -87,9 +87,9 @@ export const languages: Language[] = [
{ code: 'ar', name: 'العربية' }, // Arabic { code: 'ar', name: 'العربية' }, // Arabic
// { code: 'bg', name: 'Български' }, // Bulgarian // { code: 'bg', name: 'Български' }, // Bulgarian
// { code: 'bs', name: 'Bosanski' }, // Bosnian // { code: 'bs', name: 'Bosanski' }, // Bosnian
{ code: 'ca', name: 'Català' }, // Catalan // { code: 'ca', name: 'Català' }, // Catalan
{ code: 'cs', name: 'Čeština' }, // Czech { code: 'cs', name: 'Čeština' }, // Czech
// { code: 'da', name: 'Dansk' }, // Danish { code: 'da', name: 'Dansk' }, // Danish
{ code: 'de', name: 'Deutsch' }, // German { code: 'de', name: 'Deutsch' }, // German
// { code: 'et', name: 'Eesti' }, // Estonian // { code: 'et', name: 'Eesti' }, // Estonian
// { code: 'el', name: 'Ελληνικά' }, // Greek // { code: 'el', name: 'Ελληνικά' }, // Greek
@@ -136,13 +136,90 @@ export const languages: Language[] = [
]; ];
export const specialBlocks = { export const specialBlocks = {
'0': {
labelEvent: 'Genesis',
labelEventCompleted: 'The Genesis of Bitcoin',
networks: ['mainnet', 'testnet'],
},
'210000': {
labelEvent: 'Bitcoin\'s 1st Halving',
labelEventCompleted: 'Block Subsidy has halved to 25 BTC per block',
networks: ['mainnet', 'testnet'],
},
'420000': {
labelEvent: 'Bitcoin\'s 2nd Halving',
labelEventCompleted: 'Block Subsidy has halved to 12.5 BTC per block',
networks: ['mainnet', 'testnet'],
},
'630000': {
labelEvent: 'Bitcoin\'s 3rd Halving',
labelEventCompleted: 'Block Subsidy has halved to 6.25 BTC per block',
networks: ['mainnet', 'testnet'],
},
'709632': { '709632': {
labelEvent: 'Taproot 🌱 activation', labelEvent: 'Taproot 🌱 activation',
labelEventCompleted: 'Taproot 🌱 has been activated!', labelEventCompleted: 'Taproot 🌱 has been activated!',
networks: ['mainnet'],
}, },
'840000': { '840000': {
labelEvent: 'Halving 🥳', labelEvent: 'Bitcoin\'s 4th Halving',
labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block', labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1050000': {
labelEvent: 'Bitcoin\'s 5th Halving',
labelEventCompleted: 'Block Subsidy has halved to 1.5625 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1260000': {
labelEvent: 'Bitcoin\'s 6th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.78125 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1470000': {
labelEvent: 'Bitcoin\'s 7th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.390625 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1680000': {
labelEvent: 'Bitcoin\'s 8th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.1953125 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1890000': {
labelEvent: 'Bitcoin\'s 9th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.09765625 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2100000': {
labelEvent: 'Bitcoin\'s 10th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.04882812 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2310000': {
labelEvent: 'Bitcoin\'s 11th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.02441406 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2520000': {
labelEvent: 'Bitcoin\'s 12th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.01220703 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2730000': {
labelEvent: 'Bitcoin\'s 13th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00610351 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2940000': {
labelEvent: 'Bitcoin\'s 14th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00305175 BTC per block',
networks: ['mainnet', 'testnet'],
},
'3150000': {
labelEvent: 'Bitcoin\'s 15th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00152587 BTC per block',
networks: ['mainnet', 'testnet'],
} }
}; };

View File

@@ -24,7 +24,7 @@
<td> <td>
&lrm;{{ block.time | date:'yyyy-MM-dd HH:mm' }} &lrm;{{ block.time | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline"> <div class="lg-inline">
<i class="symbol">(<app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since>)</i> <i class="symbol">(<app-time kind="since" [time]="block.time / 1000" [fastRender]="true"></app-time>)</i>
</div> </div>
</td> </td>
</tr> </tr>

View File

@@ -17,7 +17,7 @@
<tbody *ngIf="blocks.value; else loadingTmpl"> <tbody *ngIf="blocks.value; else loadingTmpl">
<tr *ngFor="let block of blocks.value[0]; trackBy: trackByFn"> <tr *ngFor="let block of blocks.value[0]; trackBy: trackByFn">
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" [state]="{ data: { block: block } }">{{ block.height }}</a></td> <td><a [routerLink]="['/block/' | relativeUrl, block.hash]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
<td><app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since></td> <td><app-time kind="since" [time]="block.time / 1000" [fastRender]="true"></app-time></td>
<td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td> <td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
<td class="d-none d-md-block">{{ block.txs.length }}</td> <td class="d-none d-md-block">{{ block.txs.length }}</td>
</tr> </tr>

View File

@@ -35,7 +35,7 @@
<td> <td>
&lrm;{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }} &lrm;{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline"> <div class="lg-inline">
<i class="symbol">(<app-time-since [time]="bisqTx.time / 1000" [fastRender]="true"></app-time-since>)</i> <i class="symbol">(<app-time kind="since" [time]="bisqTx.time / 1000" [fastRender]="true"></app-time>)</i>
</div> </div>
</td> </td>
</tr> </tr>

View File

@@ -37,7 +37,7 @@
{{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }} <span class="d-none d-md-inline symbol">BSQ</span> {{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }} <span class="d-none d-md-inline symbol">BSQ</span>
</ng-template> </ng-template>
</td> </td>
<td><app-time-since [time]="tx.time / 1000" [fastRender]="true"></app-time-since></td> <td><app-time kind="since" [time]="tx.time / 1000" [fastRender]="true"></app-time></td>
<td class="d-none d-md-block"><a [routerLink]="['/block/' | relativeUrl, tx.blockHash]" [state]="{ data: { blockHeight: tx.blockHeight } }">{{ tx.blockHeight }}</a></td> <td class="d-none d-md-block"><a [routerLink]="['/block/' | relativeUrl, tx.blockHash]" [state]="{ data: { blockHeight: tx.blockHeight } }">{{ tx.blockHeight }}</a></td>
</tr> </tr>
</tbody> </tbody>

View File

@@ -254,3 +254,30 @@ export function selectPowerOfTen(val: number): { divider: number, unit: string }
return selectedPowerOfTen; return selectedPowerOfTen;
} }
const featureActivation = {
mainnet: {
rbf: 399701,
segwit: 477120,
taproot: 709632,
},
testnet: {
rbf: 720255,
segwit: 872730,
taproot: 2032291,
},
signet: {
rbf: 0,
segwit: 0,
taproot: 0,
},
};
export function isFeatureActive(network: string, height: number, feature: 'rbf' | 'segwit' | 'taproot'): boolean {
const activationHeight = featureActivation[network || 'mainnet']?.[feature];
if (activationHeight != null) {
return height >= activationHeight;
} else {
return false;
}
}

View File

@@ -13,7 +13,23 @@
<p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p> <p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p>
</div> </div>
<video src="/resources/mempool-promo.mp4" poster="/resources/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true"></video> <video src="/resources/promo-video/mempool-promo.mp4" poster="/resources/promo-video/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true">
<track label="English" kind="captions" srclang="en" src="/resources/promo-video/en.vtt" [attr.default]="showSubtitles('en') ? '' : null">
<track label="日本語" kind="captions" srclang="ja" src="/resources/promo-video/ja.vtt" [attr.default]="showSubtitles('ja') ? '' : null">
<track label="中文" kind="captions" srclang="zh" src="/resources/promo-video/zh.vtt" [attr.default]="showSubtitles('zh') ? '' : null">
<track label="Svenska" kind="captions" srclang="sv" src="/resources/promo-video/sv.vtt" [attr.default]="showSubtitles('sv') ? '' : null">
<track label="Čeština" kind="captions" srclang="cs" src="/resources/promo-video/cs.vtt" [attr.default]="showSubtitles('cs') ? '' : null">
<track label="Suomi" kind="captions" srclang="fi" src="/resources/promo-video/fi.vtt" [attr.default]="showSubtitles('fi') ? '' : null">
<track label="Français" kind="captions" srclang="fr" src="/resources/promo-video/fr.vtt" [attr.default]="showSubtitles('fr') ? '' : null">
<track label="Deutsch" kind="captions" srclang="de" src="/resources/promo-video/de.vtt" [attr.default]="showSubtitles('de') ? '' : null">
<track label="Italiano" kind="captions" srclang="it" src="/resources/promo-video/it.vtt" [attr.default]="showSubtitles('it') ? '' : null">
<track label="Lietuvių" kind="captions" srclang="lt" src="/resources/promo-video/lt.vtt" [attr.default]="showSubtitles('lt') ? '' : null">
<track label="Norsk" kind="captions" srclang="nb" src="/resources/promo-video/nb.vtt" [attr.default]="showSubtitles('nb') ? '' : null">
<track label="فارسی" kind="captions" srclang="fa" src="/resources/promo-video/fa.vtt" [attr.default]="showSubtitles('fa') ? '' : null">
<track label="Polski" kind="captions" srclang="pl" src="/resources/promo-video/pl.vtt" [attr.default]="showSubtitles('pl') ? '' : null">
<track label="Română" kind="captions" srclang="ro" src="/resources/promo-video/ro.vtt" [attr.default]="showSubtitles('ro') ? '' : null">
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
</video>
<div class="enterprise-sponsor" id="enterprise-sponsors"> <div class="enterprise-sponsor" id="enterprise-sponsors">
<h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3> <h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
@@ -209,7 +225,7 @@
<span>EmbassyOS</span> <span>EmbassyOS</span>
</a> </a>
<a href="https://github.com/btcpayserver/btcpayserver" target="_blank" title="BTCPay Server"> <a href="https://github.com/btcpayserver/btcpayserver" target="_blank" title="BTCPay Server">
<img class="image" src="/resources/profile/btcpayserver.svg" /> <img class="image not-rounded" src="/resources/profile/btcpayserver.svg" />
<span>BTCPay</span> <span>BTCPay</span>
</a> </a>
<a href="https://github.com/bisq-network/bisq" target="_blank" title="Bisq"> <a href="https://github.com/bisq-network/bisq" target="_blank" title="Bisq">
@@ -268,6 +284,26 @@
<img class="image" src="/resources/profile/nunchuk.svg" /> <img class="image" src="/resources/profile/nunchuk.svg" />
<span>Nunchuk</span> <span>Nunchuk</span>
</a> </a>
<a href="https://github.com/bitcoin-s/bitcoin-s" target="_blank" title="bitcoin-s">
<img class="image" src="/resources/profile/bitcoin-s.svg" />
<span>bitcoin-s</span>
</a>
<a href="https://github.com/EdgeApp" target="_blank" title="Edge">
<img class="image not-rounded" src="/resources/profile/edge.svg" />
<span>Edge</span>
</a>
<a href="https://github.com/GaloyMoney" target="_blank" title="Galoy">
<img class="image" src="/resources/profile/galoy.svg" />
<span>Galoy</span>
</a>
<a href="https://github.com/BoltzExchange" target="_blank" title="Boltz">
<img class="image" src="/resources/profile/boltz.svg" />
<span>Boltz</span>
</a>
<a href="https://github.com/MutinyWallet" target="_blank" title="Mutiny">
<img class="image not-rounded" src="/resources/profile/mutiny.svg" />
<span>Mutiny</span>
</a>
</div> </div>
</div> </div>
@@ -383,6 +419,12 @@
<a target="_blank" href="https://matrix.to/#/#mempool:bitcoin.kyoto"> <a target="_blank" href="https://matrix.to/#/#mempool:bitcoin.kyoto">
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="matrix" class="svg-inline--fa fa-matrix fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1536 1792"><path fill="currentColor" d="M40.467 163.152v1465.696H145.92V1664H0V128h145.92v35.152zm450.757 464.64v74.14h2.069c19.79-28.356 43.717-50.215 71.483-65.575 27.765-15.656 59.963-23.336 96-23.336 34.56 0 66.165 6.795 94.818 20.086 28.652 13.293 50.216 37.22 65.28 70.893 16.246-23.926 38.4-45.194 66.166-63.507 27.766-18.314 60.848-27.472 98.954-27.472 28.948 0 55.828 3.545 80.64 10.635 24.812 7.088 45.785 18.314 63.508 33.968 17.722 15.656 31.31 35.742 41.354 60.85 9.747 25.107 14.768 55.236 14.768 90.683v366.573h-150.35V865.28c0-18.314-.59-35.741-2.068-51.987-1.476-16.247-5.316-30.426-11.52-42.24-6.499-12.112-15.656-21.563-28.062-28.653-12.405-7.088-29.242-10.634-50.214-10.634-21.268 0-38.4 4.135-51.397 12.112-12.997 8.27-23.336 18.608-30.72 31.901-7.386 12.997-12.407 27.765-14.77 44.602-2.363 16.542-3.84 33.379-3.84 50.216v305.133H692.971v-307.2c0-16.247-.294-32.197-1.18-48.149-.591-15.95-3.84-30.424-9.157-44.011-5.317-13.293-14.178-24.223-26.585-32.197-12.406-7.976-30.425-12.112-54.646-12.112-7.088 0-16.542 1.478-28.062 4.726-11.52 3.25-23.04 9.157-33.968 18.02-10.93 8.86-20.383 21.563-28.063 38.103-7.68 16.543-11.52 38.4-11.52 65.28v317.834H349.44V627.792zm1004.309 1001.056V163.152H1390.08V128H1536v1536h-145.92v-35.152z"/></svg> <svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="matrix" class="svg-inline--fa fa-matrix fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1536 1792"><path fill="currentColor" d="M40.467 163.152v1465.696H145.92V1664H0V128h145.92v35.152zm450.757 464.64v74.14h2.069c19.79-28.356 43.717-50.215 71.483-65.575 27.765-15.656 59.963-23.336 96-23.336 34.56 0 66.165 6.795 94.818 20.086 28.652 13.293 50.216 37.22 65.28 70.893 16.246-23.926 38.4-45.194 66.166-63.507 27.766-18.314 60.848-27.472 98.954-27.472 28.948 0 55.828 3.545 80.64 10.635 24.812 7.088 45.785 18.314 63.508 33.968 17.722 15.656 31.31 35.742 41.354 60.85 9.747 25.107 14.768 55.236 14.768 90.683v366.573h-150.35V865.28c0-18.314-.59-35.741-2.068-51.987-1.476-16.247-5.316-30.426-11.52-42.24-6.499-12.112-15.656-21.563-28.062-28.653-12.405-7.088-29.242-10.634-50.214-10.634-21.268 0-38.4 4.135-51.397 12.112-12.997 8.27-23.336 18.608-30.72 31.901-7.386 12.997-12.407 27.765-14.77 44.602-2.363 16.542-3.84 33.379-3.84 50.216v305.133H692.971v-307.2c0-16.247-.294-32.197-1.18-48.149-.591-15.95-3.84-30.424-9.157-44.011-5.317-13.293-14.178-24.223-26.585-32.197-12.406-7.976-30.425-12.112-54.646-12.112-7.088 0-16.542 1.478-28.062 4.726-11.52 3.25-23.04 9.157-33.968 18.02-10.93 8.86-20.383 21.563-28.063 38.103-7.68 16.543-11.52 38.4-11.52 65.28v317.834H349.44V627.792zm1004.309 1001.056V163.152H1390.08V128H1536v1536h-145.92v-35.152z"/></svg>
</a> </a>
<a target="_blank" href="https://youtube.com/@mempool">
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="youtube" class="svg-inline--fa fa-youtube fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z"/></svg>
</a>
<a target="_blank" href="https://bitcointv.com/c/mempool/videos" class="bitcointv">
<svg xmlns="http://www.w3.org/2000/svg" focusable="false" viewBox="0 0 440 440"><path d="M225.57,2.08l-.69-.45a4.22,4.22,0,0,0-5.72,1.23L182.33,46.09a4,4,0,0,0,.88,5.81l9.38,6.38L173.48,97.49a4.22,4.22,0,0,0,2.45,4.19s3.55.7,4.53-1l41.92-40.56a3.62,3.62,0,0,0-1.51-5.1l-10.55-6.12L227.44,6.79A4.26,4.26,0,0,0,225.57,2.08Z" fill="currentColor"></path><path d="M118.52,401.83c-62.51,0-113.37-51-113.37-113.67V214.68C5.15,152,56,101,118.52,101H342.08a24.82,24.82,0,0,1,24.76,24.83V377a24.81,24.81,0,0,1-24.76,24.82Z"></path><path d="M342.08,105.18a20.65,20.65,0,0,1,20.61,20.66V377a20.66,20.66,0,0,1-20.61,20.66H118.52C58.3,397.67,9.31,348.55,9.31,288.16V214.68c0-60.38,49-109.5,109.21-109.5H342.08m0-8.34H118.52C53.62,96.84,1,149.6,1,214.68v73.48C1,353.24,53.62,406,118.52,406H342.08A29,29,0,0,0,371,377V125.84a29,29,0,0,0-28.92-29Z" fill="currentColor"></path><path fill="currentColor" d="M344.69,346.23A25.84,25.84,0,1,0,335,369.87l-10.22-10.2a11.69,11.69,0,1,1,4.77-5.12l10.31,10.28A25.84,25.84,0,0,0,344.69,346.23Z"></path><path fill="currentColor" d="M315.82,257.61a25.67,25.67,0,0,0-12.53,5.22L315,274.49a9.58,9.58,0,0,1,2.11-.73A9.72,9.72,0,1,1,309.4,283a9.4,9.4,0,0,1,.75-3.41L298.4,267.84a25.77,25.77,0,1,0,17.42-10.23Z"></path><path fill="currentColor" d="M313,214a7.76,7.76,0,1,1,1.41,10.91,7.62,7.62,0,0,1-2.19-2.69l-18.67-.14a25.94,25.94,0,1,0,.05-7l18.64.14A7.4,7.4,0,0,1,313,214Z"></path><path fill="currentColor" d="M341.2,144.08h-6.32c-1.67,0-3.61,1.87-3.61,4.29s1.94,4.29,3.61,4.29h6.32c1.67,0,3.61-1.87,3.61-4.29S342.87,144.08,341.2,144.08Z"></path><path fill="currentColor" d="M301.75,144.08h-6.44c-1.67,0-3.61,1.87-3.61,4.29s1.94,4.29,3.61,4.29h6.44c1.67,0,3.61-1.87,3.61-4.29S303.42,144.08,301.75,144.08Z"></path><path fill="currentColor" d="M321.77,144.08h-7c-1.67,0-3.62,1.87-3.62,4.29s1.95,4.29,3.62,4.29h7c1.67,0,3.62-1.87,3.62-4.29S323.44,144.08,321.77,144.08Z"></path><ellipse fill="currentColor" cx="295.97" cy="127.61" rx="4.27" ry="4.29"></ellipse><path fill="currentColor" d="M340.54,131.9a4.29,4.29,0,1,0-4.27-4.29A4.28,4.28,0,0,0,340.54,131.9Z"></path><path fill="currentColor" d="M318.26,131.9a4.29,4.29,0,1,0-4.27-4.29A4.29,4.29,0,0,0,318.26,131.9Z"></path><ellipse fill="currentColor" cx="295.97" cy="169.13" rx="4.27" ry="4.29"></ellipse><path fill="currentColor" d="M340.54,164.84a4.3,4.3,0,1,0,4.27,4.29A4.29,4.29,0,0,0,340.54,164.84Z"></path><path fill="currentColor" d="M318.26,164.84a4.3,4.3,0,1,0,4.28,4.29A4.29,4.29,0,0,0,318.26,164.84Z"></path><path d="M108.62,256.87c8.36-1,7.68-7.76,3.14-17-3.64-7.4-9.74-16.39-15.75-25.36-14.23-21.23-27.69-42.23-5.35-41.07,19.55,1,42.9,18.63,68.22,36.74,31.1,22.24,65.16,45.21,98.81,39.11a151.19,151.19,0,0,1,20-2.37V221a92,92,0,0,0-91.91-92.16H124.33A92,92,0,0,0,32.42,221v17.59c17.71,3.81,31,9.94,43.8,14.15C86.6,256.16,96.69,258.31,108.62,256.87Z"></path><path d="M273.37,310.79c-35-15.26-76.67-32.1-104-23.59-3.15,1-5,2.3-6,3.85-3.35,5.31,4.67,13.57,14.89,22.17,7.17,6,15.36,12.21,21.44,17.64,11.47,10.26,15.35,17.84-9.89,16.62-29.75-1.44-49.18-13.75-71.18-24l-.29-.14a165.84,165.84,0,0,0-22.93-8.91c-15.74-4.67-34.22-6.79-58.51-3.28A91.93,91.93,0,0,0,124.33,375h61.45A92,92,0,0,0,273.37,310.79Z"></path><path fill="currentColor" d="M257.69,249.31C224,255.41,190,232.44,158.88,210.2c-25.32-18.11-48.67-35.72-68.22-36.74C68.32,172.3,81.78,193.3,96,214.53c6,9,12.11,18,15.75,25.36,4.54,9.22,5.22,16-3.14,17-11.93,1.44-22-.71-32.4-4.13-12.8-4.21-26.09-10.34-43.8-14.15v44.26c0,1.26.14,2.48.19,3.72a91.8,91.8,0,0,0,2.9,19.62c.43,1.67.84,3.34,1.37,5,24.29-3.51,42.77-1.39,58.51,3.28a165.84,165.84,0,0,1,22.93,8.91c.39-.12.76-.26,1.14-.39l-.85.53c22,10.25,41.43,22.56,71.18,24,25.24,1.22,21.36-6.36,9.89-16.62-6.08-5.43-14.27-11.61-21.44-17.64-10.22-8.6-18.24-16.86-14.89-22.17,1-1.55,2.87-2.87,6-3.85,27.33-8.51,69,8.33,104,23.59.32-1,.56-2.05.84-3.07a92.33,92.33,0,0,0,3.48-24.87V246.94A151.19,151.19,0,0,0,257.69,249.31Z"></path><path fill="currentColor" d="M192,137a78,78,0,0,1,77.78,78v73.91a78,78,0,0,1-77.78,78H118.51a78,78,0,0,1-77.78-78V215a78,78,0,0,1,77.78-78H192m0-8.33H118.51A86.21,86.21,0,0,0,32.42,215v73.91a86.21,86.21,0,0,0,86.09,86.33H192a86.21,86.21,0,0,0,86.09-86.33V215A86.21,86.21,0,0,0,192,128.64Z"></path></svg>
</a>
</div> </div>
</div> </div>

View File

@@ -11,6 +11,12 @@
line-height: 32px; line-height: 32px;
} }
.image.not-rounded {
border-radius: 0;
width: 60px;
height: 60px;
}
.intro { .intro {
margin: 25px auto 30px; margin: 25px auto 30px;
width: 250px; width: 250px;
@@ -36,9 +42,11 @@
video { video {
width: 640px; width: 640px;
height: 360px;
max-width: 90%; max-width: 90%;
margin-top: 0; margin-top: 0;
@media (min-width: 768px) {
height: 360px;
}
} }
.social-icons { .social-icons {
@@ -51,9 +59,13 @@
.enterprise-sponsor, .enterprise-sponsor,
.community-integrations-sponsor, .community-integrations-sponsor,
.maintainers { .maintainers {
margin-top: 68px; margin-top: 30px;
margin-bottom: 68px; margin-bottom: 68px;
scroll-margin: 30px; scroll-margin: 30px;
@media (min-width: 768px) {
margin-top: 68px;
}
} }
.maintainers { .maintainers {
@@ -199,6 +211,16 @@
a { a {
margin: 45px 10px; margin: 45px 10px;
} }
.bitcointv svg {
width: 36px;
height: auto;
vertical-align: bottom;
margin-bottom: 2px;
margin-left: 5px;
}
.bitcointv svg:hover {
opacity: 0.75;
}
} }
} }
@@ -212,6 +234,11 @@
} }
.community-integrations-sponsor { .community-integrations-sponsor {
max-width: 965px; max-width: 1110px;
margin: auto; margin: auto;
} }
.community-integrations-sponsor img.image {
width: 64px;
height: 64px;
}

View File

@@ -68,7 +68,7 @@ export class AboutComponent implements OnInit {
tap(() => this.goToAnchor()) tap(() => this.goToAnchor())
); );
} }
ngAfterViewInit() { ngAfterViewInit() {
this.goToAnchor(); this.goToAnchor();
} }
@@ -90,4 +90,8 @@ export class AboutComponent implements OnInit {
this.showNavigateToSponsor = true; this.showNavigateToSponsor = true;
} }
} }
showSubtitles(language) {
return ( this.locale.startsWith( language ) && !this.locale.startsWith('en') );
}
} }

View File

@@ -1,15 +1,15 @@
<ng-container *ngIf="!noFiat && (viewFiat$ | async) && (conversions$ | async) as conversions; else viewFiatVin"> <ng-container *ngIf="!noFiat && (viewFiat$ | async) && (conversions$ | async) as conversions; else viewFiatVin">
<span class="fiat" *ngIf="blockConversion; else noblockconversion"> <span class="fiat" *ngIf="blockConversion; else noblockconversion">
{{ addPlus && satoshis >= 0 ? '+' : '' }} {{ addPlus && satoshis >= 0 ? '+' : '' }}{{
{{
( (
(blockConversion.price[currency] >= 0 ? blockConversion.price[currency] : null) ?? (blockConversion.price[currency] > -1 ? blockConversion.price[currency] : null) ??
(blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency]) ?? 0 (blockConversion.price['USD'] > -1 ? blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency] : null) ?? 0
) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency ) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency
}} }}
</span> </span>
<ng-template #noblockconversion> <ng-template #noblockconversion>
<span class="fiat">{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ (conversions ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }}</span> <span class="fiat">{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ (conversions[currency] > -1 ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }}
</span>
</ng-template> </ng-template>
</ng-container> </ng-container>

View File

@@ -54,31 +54,6 @@
max-height: 270px; max-height: 270px;
} }
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.disabled { .disabled {
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;

View File

@@ -54,31 +54,6 @@
max-height: 270px; max-height: 270px;
} }
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.disabled { .disabled {
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;

View File

@@ -54,31 +54,6 @@
max-height: 270px; max-height: 270px;
} }
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.disabled { .disabled {
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;

View File

@@ -54,31 +54,6 @@
max-height: 270px; max-height: 270px;
} }
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 1130px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 1130px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.disabled { .disabled {
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;

View File

@@ -121,7 +121,7 @@
<ng-container *ngIf="!isLoadingBlock; else loadingRest"> <ng-container *ngIf="!isLoadingBlock; else loadingRest">
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'"> <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td i18n="mempool-block.fee-span">Fee span</td> <td i18n="mempool-block.fee-span">Fee span</td>
<td><span>{{ block.extras.feeRange[0] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td> <td><span>{{ block.extras.feeRange[1] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
</tr> </tr>
<tr *ngIf="block?.extras?.medianFee != undefined"> <tr *ngIf="block?.extras?.medianFee != undefined">
<td class="td-width" i18n="block.median-fee">Median fee</td> <td class="td-width" i18n="block.median-fee">Median fee</td>

View File

@@ -6,7 +6,7 @@
<div [attr.data-cy]="'bitcoin-block-offset-' + offset + '-index-' + i" <div [attr.data-cy]="'bitcoin-block-offset-' + offset + '-index-' + i"
class="text-center bitcoin-block mined-block blockchain-blocks-offset-{{ offset }}-index-{{ i }}" class="text-center bitcoin-block mined-block blockchain-blocks-offset-{{ offset }}-index-{{ i }}"
id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"
[class.blink-bg]="(specialBlocks[block.height] !== undefined)"> [class.blink-bg]="isSpecial(block.height)">
<a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }" <a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }"
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}">&nbsp;</a> class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}">&nbsp;</a>
<div [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height"> <div [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height">
@@ -47,7 +47,7 @@
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template> <ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
</div> </div>
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-time'" class="time-difference"> <div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-time'" class="time-difference">
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></div> <app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></div>
</div> </div>
<div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined"> <div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary" <a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary"

View File

@@ -20,8 +20,8 @@ interface BlockchainBlock extends BlockExtended {
export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
@Input() static: boolean = false; @Input() static: boolean = false;
@Input() offset: number = 0; @Input() offset: number = 0;
@Input() height: number = 0; @Input() height: number = 0; // max height of blocks in chunk (dynamic blocks only)
@Input() count: number = 8; @Input() count: number = 8; // number of blocks in this chunk (dynamic blocks only)
@Input() loadingTip: boolean = false; @Input() loadingTip: boolean = false;
@Input() connected: boolean = true; @Input() connected: boolean = true;
@@ -31,6 +31,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
dynamicBlocksAmount: number = 8; dynamicBlocksAmount: number = 8;
emptyBlocks: BlockExtended[] = this.mountEmptyBlocks(); emptyBlocks: BlockExtended[] = this.mountEmptyBlocks();
markHeight: number; markHeight: number;
chainTip: number;
blocksSubscription: Subscription; blocksSubscription: Subscription;
blockPageSubscription: Subscription; blockPageSubscription: Subscription;
networkSubscription: Subscription; networkSubscription: Subscription;
@@ -73,6 +74,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
} }
ngOnInit() { ngOnInit() {
this.chainTip = this.stateService.latestBlockHeight;
this.dynamicBlocksAmount = Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT); this.dynamicBlocksAmount = Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT);
if (['', 'testnet', 'signet'].includes(this.stateService.network)) { if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
@@ -107,7 +109,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.blocks.unshift(block); this.blocks.unshift(block);
this.blocks = this.blocks.slice(0, this.dynamicBlocksAmount); this.blocks = this.blocks.slice(0, this.dynamicBlocksAmount);
if (txConfirmed) { if (txConfirmed && block.height > this.chainTip) {
this.markHeight = block.height; this.markHeight = block.height;
this.moveArrowToPosition(true, true); this.moveArrowToPosition(true, true);
} else { } else {
@@ -115,7 +117,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
} }
this.blockStyles = []; this.blockStyles = [];
if (this.blocksFilled) { if (this.blocksFilled && block.height > this.chainTip) {
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -155 : -205))); this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -155 : -205)));
setTimeout(() => { setTimeout(() => {
this.blockStyles = []; this.blockStyles = [];
@@ -129,6 +131,8 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
if (this.blocks.length === this.dynamicBlocksAmount) { if (this.blocks.length === this.dynamicBlocksAmount) {
this.blocksFilled = true; this.blocksFilled = true;
} }
this.chainTip = Math.max(this.chainTip, block.height);
this.cd.markForCheck(); this.cd.markForCheck();
}); });
} else { } else {
@@ -265,6 +269,10 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.cd.markForCheck(); this.cd.markForCheck();
} }
isSpecial(height: number): boolean {
return this.specialBlocks[height]?.networks.includes(this.stateService.network || 'mainnet') ? true : false;
}
getStyleForBlock(block: BlockchainBlock, index: number, animateEnterFrom: number = 0) { getStyleForBlock(block: BlockchainBlock, index: number, animateEnterFrom: number = 0) {
if (!block || block.placeholder) { if (!block || block.placeholder) {
return this.getStyleForPlaceholderBlock(index, animateEnterFrom); return this.getStyleForPlaceholderBlock(index, animateEnterFrom);

View File

@@ -26,7 +26,7 @@
</thead> </thead>
<tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''"> <tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock"> <tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
<td class="text-left" [class]="widget ? 'widget' : ''"> <td class="height text-left" [class]="widget ? 'widget' : ''">
<a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a> <a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a>
</td> </td>
<td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> <td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
@@ -43,7 +43,7 @@
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} &lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
</td> </td>
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'"> <td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since> <app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time>
</td> </td>
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> <td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<a <a
@@ -89,7 +89,6 @@
<span class="skeleton-loader" style="max-width: 75px"></span> <span class="skeleton-loader" style="max-width: 75px"></span>
</td> </td>
<td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> <td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<img width="1" height="25" style="opacity: 0">
<span class="skeleton-loader" style="max-width: 125px"></span> <span class="skeleton-loader" style="max-width: 125px"></span>
</td> </td>
<td class="timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'"> <td class="timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
@@ -98,7 +97,7 @@
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'"> <td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
<span class="skeleton-loader" style="max-width: 125px"></span> <span class="skeleton-loader" style="max-width: 125px"></span>
</td> </td>
<td *ngIf="auditAvailable" class="health text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> <td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<span class="skeleton-loader" style="max-width: 75px"></span> <span class="skeleton-loader" style="max-width: 75px"></span>
</td> </td>
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> <td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">

View File

@@ -51,7 +51,12 @@ tr, td, th {
.pool.widget { .pool.widget {
width: 40%; width: 40%;
padding-left: 24px; padding-left: 24px;
@media (max-width: 376px) { @media (min-width: 768px) AND (max-width: 926px) {
padding-left: 0px;
width: 60%;
}
@media (max-width: 430px) {
padding-left: 0px;
width: 60%; width: 60%;
} }
} }
@@ -59,6 +64,10 @@ tr, td, th {
display: inline-block; display: inline-block;
vertical-align: text-top; vertical-align: text-top;
padding-left: 10px; padding-left: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 160px;
} }
.height { .height {
@@ -69,6 +78,12 @@ tr, td, th {
@media (max-width: 576px) { @media (max-width: 576px) {
width: 10%; width: 10%;
} }
@media (min-width: 768px) AND (max-width: 926px) {
width: 30%;
}
@media (max-width: 430px) {
width: 30%;
}
} }
.height.legacy { .height.legacy {
width: 15%; width: 15%;
@@ -92,7 +107,7 @@ tr, td, th {
.mined { .mined {
width: 13%; width: 13%;
@media (max-width: 576px) { @media (max-width: 730px) {
display: none; display: none;
} }
} }
@@ -138,7 +153,7 @@ tr, td, th {
.fees { .fees {
width: 8%; width: 8%;
@media (max-width: 650px) { @media (max-width: 820px) {
display: none; display: none;
} }
} }
@@ -163,6 +178,16 @@ tr, td, th {
width: 30%; width: 30%;
padding-right: 0; padding-right: 0;
} }
@media (min-width: 768px) AND (max-width: 926px) {
overflow: hidden;
text-overflow: ellipsis;
max-width: 90px;
}
@media (max-width: 430px) {
overflow: hidden;
text-overflow: ellipsis;
max-width: 90px;
}
} }
.size { .size {
@@ -189,10 +214,10 @@ tr, td, th {
.health { .health {
width: 10%; width: 10%;
@media (max-width: 1000px) { @media (max-width: 1105px) {
width: 13%; width: 13%;
} }
@media (max-width: 950px) { @media (max-width: 560px) {
display: none; display: none;
} }
@@ -202,7 +227,7 @@ tr, td, th {
} }
.health.widget { .health.widget {
width: 25%; width: 25%;
@media (max-width: 1000px) { @media (max-width: 1105px) {
display: none; display: none;
} }
@media (max-width: 767px) { @media (max-width: 767px) {
@@ -242,4 +267,4 @@ tr, td, th {
vertical-align: middle; vertical-align: middle;
max-width: 50vw; max-width: 50vw;
text-align: left; text-align: left;
} }

View File

@@ -87,8 +87,8 @@ export class BlocksList implements OnInit, OnDestroy {
this.stateService.blocks$ this.stateService.blocks$
.pipe( .pipe(
switchMap((block) => { switchMap((block) => {
if (block[0].height < this.lastBlockHeight) { if (block[0].height <= this.lastBlockHeight) {
return []; // Return an empty stream so the last pipe is not executed return [null]; // Return an empty stream so the last pipe is not executed
} }
this.lastBlockHeight = block[0].height; this.lastBlockHeight = block[0].height;
return [block]; return [block];
@@ -101,14 +101,16 @@ export class BlocksList implements OnInit, OnDestroy {
this.lastPage = this.page; this.lastPage = this.page;
return blocks[0]; return blocks[0];
} }
this.blocksCount = Math.max(this.blocksCount, blocks[1][0].height) + 1; if (blocks[1]) {
if (this.stateService.env.MINING_DASHBOARD) { this.blocksCount = Math.max(this.blocksCount, blocks[1][0].height) + 1;
// @ts-ignore: Need to add an extra field for the template if (this.stateService.env.MINING_DASHBOARD) {
blocks[1][0].extras.pool.logo = `/resources/mining-pools/` + // @ts-ignore: Need to add an extra field for the template
blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'; blocks[1][0].extras.pool.logo = `/resources/mining-pools/` +
blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
}
acc.unshift(blocks[1][0]);
acc = acc.slice(0, this.widget ? 6 : 15);
} }
acc.unshift(blocks[1][0]);
acc = acc.slice(0, this.widget ? 6 : 15);
return acc; return acc;
}, []) }, [])
); );

View File

@@ -2,18 +2,19 @@
<table class="table latest-adjustments"> <table class="table latest-adjustments">
<thead> <thead>
<tr> <tr>
<th class="d-none d-md-block" i18n="block.height">Height</th> <th class="" i18n="block.height">Height</th>
<th i18n="mining.adjusted" class="text-left">Adjusted</th> <th class="date text-left" i18n="mining.adjusted">Adjusted</th>
<th i18n="mining.difficulty" class="text-right">Difficulty</th> <th class="text-right" i18n="mining.difficulty">Difficulty</th>
<th i18n="mining.change" class="text-right">Change</th> <th class="text-right" i18n="mining.change">Change</th>
</tr> </tr>
</thead> </thead>
<tbody *ngIf="(hashrateObservable$ | async) as data"> <tbody *ngIf="(hashrateObservable$ | async) as data">
<tr *ngFor="let diffChange of data"> <tr *ngFor="let diffChange of data">
<td class="d-none d-md-block"><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height <td class="">
}}</a></td> <a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a>
<td class="text-left"> </td>
<app-time-since [time]="diffChange.timestamp" [fastRender]="true"></app-time-since> <td class="date text-left">
<app-time kind="since" [time]="diffChange.timestamp" [fastRender]="true"></app-time>
</td> </td>
<td class="text-right">{{ diffChange.difficultyShorten }}</td> <td class="text-right">{{ diffChange.difficultyShorten }}</td>
<td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'"> <td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">
@@ -23,8 +24,8 @@
</tbody> </tbody>
<tbody *ngIf="isLoading"> <tbody *ngIf="isLoading">
<tr *ngFor="let item of [1,2,3,4,5,6]"> <tr *ngFor="let item of [1,2,3,4,5,6]">
<td class="d-none d-md-block w-75"><span class="skeleton-loader"></span></td> <td class=""><span class="skeleton-loader"></span></td>
<td class="text-left"><span class="skeleton-loader w-75"></span></td> <td class="date text-left"><span class="skeleton-loader w-75"></span></td>
<td class="text-right"><span class="skeleton-loader w-75"></span></td> <td class="text-right"><span class="skeleton-loader w-75"></span></td>
<td class="text-right"><span class="skeleton-loader w-75"></span></td> <td class="text-right"><span class="skeleton-loader w-75"></span></td>
</tr> </tr>

View File

@@ -17,3 +17,12 @@
} }
} }
} }
.date {
@media (min-width: 767px) AND (max-width: 991px) {
display: none;
}
@media (max-width: 500px) {
display: none;
}
}

View File

@@ -0,0 +1,88 @@
<div *ngIf="showTitle" class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
<div class="card-wrapper">
<div class="card">
<div class="card-body more-padding">
<div class="difficulty-adjustment-container" *ngIf="(isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData; else loadingDifficulty">
<div class="item">
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
<div class="card-text">
<ng-container *ngTemplateOutlet="epochData.remainingBlocks === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.remainingBlocks }"></ng-container>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
</div>
<div class="symbol"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time></div>
</div>
<div class="item">
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span>
<ng-template #arrowDownDifficulty >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
</ng-template>
{{ epochData.change | absolute | number: '1.2-2' }}
<span class="symbol">%</span>
</div>
<ng-template #recentlyAdjusted>
<div class="card-text">&#8212;</div>
</ng-template>
<div class="symbol">
<span i18n="difficulty-box.previous">Previous</span>:
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
<span *ngIf="epochData.previousRetarget > 0; else arrowDownPreviousDifficulty" >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span>
<ng-template #arrowDownPreviousDifficulty >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
</ng-template>
{{ epochData.previousRetarget | absolute | number: '1.2-2' }} </span> %
</div>
</div>
<div class="item" *ngIf="showProgress">
<h5 class="card-title" i18n="difficulty-box.current-period">Current Period</h5>
<div class="card-text">{{ epochData.progress | number: '1.2-2' }} <span class="symbol">%</span></div>
<div class="progress small-bar">
<div class="progress-bar" role="progressbar" style="width: 15%; background-color: #105fb0" [ngStyle]="{'width': epochData.base}">&nbsp;</div>
</div>
</div>
<div class="item" *ngIf="showHalving">
<h5 class="card-title" i18n="difficulty-box.next-halving" i18n-ngbTooltip="difficulty-box.next-halving"
ngbTooltip="Next Halving" placement="bottom" #averagefee [disableTooltip]="!isEllipsisActive(averagefee)">Next Halving</h5>
<div class="card-text">
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
</div>
<div class="symbol"><app-time kind="until" [time]="epochData.timeUntilHalving" [fastRender]="true"></app-time></div>
</div>
</div>
</div>
</div>
</div>
<ng-template #loadingDifficulty>
<div class="difficulty-skeleton loading-container">
<div class="item">
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
</div>
</ng-template>

View File

@@ -0,0 +1,159 @@
.difficulty-adjustment-container {
display: flex;
flex-direction: row;
justify-content: space-around;
height: 76px;
.shared-block {
color: #ffffff66;
font-size: 12px;
}
.item {
padding: 0 5px;
width: 100%;
max-width: 150px;
&:nth-child(1) {
display: none;
@media (min-width: 485px) {
display: table-cell;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: table-cell;
}
}
}
.card-text {
font-size: 22px;
margin-top: -9px;
position: relative;
}
}
.difficulty-skeleton {
display: flex;
justify-content: space-between;
@media (min-width: 376px) {
flex-direction: row;
}
.item {
min-width: 120px;
max-width: 150px;
margin: 0;
width: -webkit-fill-available;
@media (min-width: 376px) {
margin: 0 auto 0px;
}
&:first-child{
display: none;
@media (min-width: 485px) {
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
&:last-child {
margin-bottom: 0;
}
}
.card-text {
.skeleton-loader {
width: 100%;
display: block;
&:first-child {
margin: 14px auto 0;
max-width: 80px;
}
&:last-child {
margin: 10px auto 0;
max-width: 120px;
}
}
}
}
.card {
background-color: #1d1f31;
height: 100%;
}
.card-title {
color: #4a68b9;
font-size: 1rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.progress {
display: inline-flex;
width: 100%;
background-color: #2d3348;
height: 1.1rem;
max-width: 180px;
}
.skeleton-loader {
max-width: 100%;
}
.more-padding {
padding: 18px;
}
.small-bar {
height: 8px;
top: -4px;
max-width: 120px;
}
.loading-container {
min-height: 76px;
}
.main-title {
position: relative;
color: #ffffff91;
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;
font-weight: 500;
text-align: center;
padding-bottom: 3px;
}
.card-wrapper {
.card {
height: auto !important;
}
.card-body {
display: flex;
flex: inherit;
text-align: center;
flex-direction: column;
justify-content: space-around;
padding: 24px 20px;
}
}
.retarget-sign {
margin-right: -3px;
font-size: 14px;
top: -2px;
position: relative;
}
.previous-retarget-sign {
margin-right: -2px;
font-size: 10px;
}
.symbol {
font-size: 13px;
}

View File

@@ -0,0 +1,90 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { combineLatest, Observable, timer } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { StateService } from '../../services/state.service';
interface EpochProgress {
base: string;
change: number;
progress: number;
remainingBlocks: number;
newDifficultyHeight: number;
colorAdjustments: string;
colorPreviousAdjustments: string;
estimatedRetargetDate: number;
previousRetarget: number;
blocksUntilHalving: number;
timeUntilHalving: number;
}
@Component({
selector: 'app-difficulty-mining',
templateUrl: './difficulty-mining.component.html',
styleUrls: ['./difficulty-mining.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DifficultyMiningComponent implements OnInit {
isLoadingWebSocket$: Observable<boolean>;
difficultyEpoch$: Observable<EpochProgress>;
@Input() showProgress = true;
@Input() showHalving = false;
@Input() showTitle = true;
constructor(
public stateService: StateService,
) { }
ngOnInit(): void {
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.difficultyEpoch$ = combineLatest([
this.stateService.blocks$.pipe(map(([block]) => block)),
this.stateService.difficultyAdjustment$,
])
.pipe(
map(([block, da]) => {
let colorAdjustments = '#ffffff66';
if (da.difficultyChange > 0) {
colorAdjustments = '#3bcc49';
}
if (da.difficultyChange < 0) {
colorAdjustments = '#dc3545';
}
let colorPreviousAdjustments = '#dc3545';
if (da.previousRetarget) {
if (da.previousRetarget >= 0) {
colorPreviousAdjustments = '#3bcc49';
}
if (da.previousRetarget === 0) {
colorPreviousAdjustments = '#ffffff66';
}
} else {
colorPreviousAdjustments = '#ffffff66';
}
const blocksUntilHalving = 210000 - (block.height % 210000);
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
const data = {
base: `${da.progressPercent.toFixed(2)}%`,
change: da.difficultyChange,
progress: da.progressPercent,
remainingBlocks: da.remainingBlocks - 1,
colorAdjustments,
colorPreviousAdjustments,
newDifficultyHeight: da.nextRetargetHeight,
estimatedRetargetDate: da.estimatedRetargetDate,
previousRetarget: da.previousRetarget,
blocksUntilHalving,
timeUntilHalving,
};
return data;
})
);
}
isEllipsisActive(e): boolean {
return (e.offsetWidth < e.scrollWidth);
}
}

View File

@@ -0,0 +1,41 @@
<div
#tooltip
*ngIf="status"
class="difficulty-tooltip"
[style.visibility]="status ? 'visible' : 'hidden'"
[style.left]="tooltipPosition.x + 'px'"
[style.top]="tooltipPosition.y + 'px'"
>
<ng-container [ngSwitch]="status">
<ng-container *ngSwitchCase="'mined'">
<ng-container *ngIf="isAhead">
<ng-container *ngTemplateOutlet="expected === 1 ? blocksSingular : blocksPlural; context: {$implicit: expected }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.expected-blocks">{{ i }} blocks expected</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.expected-block">{{ i }} block expected</ng-template>
</ng-container>
<ng-container *ngIf="!isAhead">
<ng-container *ngTemplateOutlet="mined === 1 ? blocksSingular : blocksPlural; context: {$implicit: mined }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.mined-blocks">{{ i }} blocks mined</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.mined-block">{{ i }} block mined</ng-template>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'remaining'">
<ng-container *ngTemplateOutlet="remaining === 1 ? blocksSingular : blocksPlural; context: {$implicit: remaining }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.remaining-blocks">{{ i }} blocks remaining</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.remaining-block">{{ i }} block remaining</ng-template>
</ng-container>
<ng-container *ngSwitchCase="'ahead'">
<ng-container *ngTemplateOutlet="ahead === 1 ? blocksSingular : blocksPlural; context: {$implicit: ahead }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.blocks-ahead">{{ i }} blocks ahead</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.block-ahead">{{ i }} block ahead</ng-template>
</ng-container>
<ng-container *ngSwitchCase="'behind'">
<ng-container *ngTemplateOutlet="behind === 1 ? blocksSingular : blocksPlural; context: {$implicit: behind }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.blocks-behind">{{ i }} blocks behind</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.block-behind">{{ i }} block behind</ng-template>
</ng-container>
<ng-container *ngSwitchCase="'next'">
<span class="next-block" i18n="@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c">Next Block</span>
</ng-container>
</ng-container>
</div>

View File

@@ -0,0 +1,22 @@
.difficulty-tooltip {
position: fixed;
background: rgba(#11131f, 0.95);
border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
color: #b1b1b1;
padding: 10px 15px;
text-align: left;
pointer-events: none;
max-width: 300px;
min-width: 200px;
text-align: center;
p {
margin: 0;
white-space: nowrap;
}
}
.next-block {
text-transform: lowercase;
}

View File

@@ -0,0 +1,66 @@
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
interface EpochProgress {
base: string;
change: number;
progress: number;
minedBlocks: number;
remainingBlocks: number;
expectedBlocks: number;
newDifficultyHeight: number;
colorAdjustments: string;
colorPreviousAdjustments: string;
estimatedRetargetDate: number;
previousRetarget: number;
blocksUntilHalving: number;
timeUntilHalving: number;
}
const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
@Component({
selector: 'app-difficulty-tooltip',
templateUrl: './difficulty-tooltip.component.html',
styleUrls: ['./difficulty-tooltip.component.scss'],
})
export class DifficultyTooltipComponent implements OnChanges {
@Input() status: string | void;
@Input() progress: EpochProgress | void = null;
@Input() cursorPosition: { x: number, y: number };
mined: number;
ahead: number;
behind: number;
expected: number;
remaining: number;
isAhead: boolean;
isBehind: boolean;
tooltipPosition = { x: 0, y: 0 };
@ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>;
constructor() {}
ngOnChanges(changes): void {
if (changes.cursorPosition && changes.cursorPosition.currentValue) {
let x = changes.cursorPosition.currentValue.x;
let y = changes.cursorPosition.currentValue.y - 50;
if (this.tooltipElement) {
const elementBounds = this.tooltipElement.nativeElement.getBoundingClientRect();
x -= elementBounds.width / 2;
x = Math.min(Math.max(x, 20), (window.innerWidth - 20 - elementBounds.width));
}
this.tooltipPosition = { x, y };
}
if ((changes.progress || changes.status) && this.progress && this.status) {
this.remaining = this.progress.remainingBlocks;
this.expected = this.progress.expectedBlocks;
this.mined = this.progress.minedBlocks;
this.ahead = Math.max(0, this.mined - this.expected);
this.behind = Math.max(0, this.expected - this.mined);
this.isAhead = this.ahead > 0;
this.isBehind = this.behind > 0;
}
}
}

View File

@@ -3,81 +3,100 @@
<div class="card"> <div class="card">
<div class="card-body more-padding"> <div class="card-body more-padding">
<div class="difficulty-adjustment-container" *ngIf="(isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData; else loadingDifficulty"> <div class="difficulty-adjustment-container" *ngIf="(isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData; else loadingDifficulty">
<div class="item"> <div class="epoch-progress">
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5> <svg class="epoch-blocks" height="22px" width="100%" viewBox="0 0 224 9" shape-rendering="crispEdges" preserveAspectRatio="none">
<div class="card-text"> <defs>
<ng-container *ngTemplateOutlet="epochData.remainingBlocks === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.remainingBlocks }"></ng-container> <linearGradient id="diff-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template> <stop offset="0%" stop-color="#105fb0" />
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template> <stop offset="100%" stop-color="#9339f4" />
</div> </linearGradient>
<div class="symbol"><app-time-until [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time-until></div> <linearGradient id="diff-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#2486eb" />
<stop offset="100%" stop-color="#ae6af7" />
</linearGradient>
</defs>
<rect
*ngFor="let rect of shapes"
[attr.x]="rect.x" [attr.y]="rect.y"
[attr.width]="rect.w" [attr.height]="rect.h"
class="rect {{rect.status}}"
[class.hover]="hoverSection && rect.status === hoverSection.status"
(pointerover)="onHover($event, rect);"
(pointerout)="onBlur($event);"
>
<animate
*ngIf="rect.status === 'next'"
attributeType="XML"
attributeName="fill"
[attr.values]="'#fff;' + (rect.expected ? '#D81B60' : '#2d3348') + ';#fff'"
dur="2s"
repeatCount="indefinite"/>
</rect>
</svg>
</div> </div>
<div class="item"> <div class="difficulty-stats">
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5> <div class="item">
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}"> <div class="card-text">
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" > ~<app-time [time]="epochData.timeAvg / 1000" [forceFloorOnTimeIntervals]="['minute']" [fractionDigits]="1"></app-time>
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon> </div>
</span> <div class="symbol" i18n="difficulty-box.average-block-time">Average block time</div>
<ng-template #arrowDownDifficulty >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
</ng-template>
{{ epochData.change | absolute | number: '1.2-2' }}
<span class="symbol">%</span>
</div> </div>
<ng-template #recentlyAdjusted> <div class="item">
<div class="card-text">&#8212;</div> <div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
</ng-template> <span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
<div class="symbol"> <fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
<span i18n="difficulty-box.previous">Previous</span>:
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
<span *ngIf="epochData.previousRetarget > 0; else arrowDownPreviousDifficulty" >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span> </span>
<ng-template #arrowDownPreviousDifficulty > <ng-template #arrowDownDifficulty >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon> <fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
</ng-template> </ng-template>
{{ epochData.previousRetarget | absolute | number: '1.2-2' }} </span> % {{ epochData.change | absolute | number: '1.2-2' }}
<span class="symbol">%</span>
</div>
<ng-template #recentlyAdjusted>
<div class="card-text">&#8212;</div>
</ng-template>
<div class="symbol">
<span i18n="difficulty-box.previous">Previous</span>:
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
<span *ngIf="epochData.previousRetarget > 0; else arrowDownPreviousDifficulty" >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span>
<ng-template #arrowDownPreviousDifficulty >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
</ng-template>
{{ epochData.previousRetarget | absolute | number: '1.2-2' }} </span> %
</div>
</div> </div>
</div> <div class="item">
<div class="item" *ngIf="showProgress"> <div class="card-text"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time></div>
<h5 class="card-title" i18n="difficulty-box.current-period">Current Period</h5> <div class="symbol">
<div class="card-text">{{ epochData.progress | number: '1.2-2' }} <span class="symbol">%</span></div> {{ epochData.retargetDateString }}
<div class="progress small-bar"> </div>
<div class="progress-bar" role="progressbar" style="width: 15%; background-color: #105fb0" [ngStyle]="{'width': epochData.base}">&nbsp;</div>
</div> </div>
</div> </div>
<div class="item" *ngIf="showHalving">
<h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
<div class="card-text">
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
</div>
<div class="symbol"><app-time-until [time]="epochData.timeUntilHalving" [fastRender]="true"></app-time-until></div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<ng-template #loadingDifficulty> <ng-template #loadingDifficulty>
<div class="epoch-progress">
<div class="skeleton-loader"></div>
</div>
<div class="difficulty-skeleton loading-container"> <div class="difficulty-skeleton loading-container">
<div class="item"> <div class="item">
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
<div class="card-text"> <div class="card-text">
<div class="skeleton-loader"></div> <div class="skeleton-loader"></div>
<div class="skeleton-loader"></div> <div class="skeleton-loader"></div>
</div> </div>
</div> </div>
<div class="item"> <div class="item">
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
<div class="card-text"> <div class="card-text">
<div class="skeleton-loader"></div> <div class="skeleton-loader"></div>
<div class="skeleton-loader"></div> <div class="skeleton-loader"></div>
</div> </div>
</div> </div>
<div class="item"> <div class="item">
<h5 class="card-title" i18n="difficulty-box.current-period">Current Period</h5>
<div class="card-text"> <div class="card-text">
<div class="skeleton-loader"></div> <div class="skeleton-loader"></div>
<div class="skeleton-loader"></div> <div class="skeleton-loader"></div>
@@ -85,3 +104,10 @@
</div> </div>
</div> </div>
</ng-template> </ng-template>
<app-difficulty-tooltip
*ngIf="hoverSection && (isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData"
[cursorPosition]="tooltipPosition"
[status]="hoverSection.status"
[progress]="epochData"
></app-difficulty-tooltip>

View File

@@ -1,8 +1,14 @@
.difficulty-adjustment-container { .difficulty-adjustment-container {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.difficulty-stats {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-around; justify-content: space-around;
height: 76px; height: 50.5px;
.shared-block { .shared-block {
color: #ffffff66; color: #ffffff66;
font-size: 12px; font-size: 12px;
@@ -24,8 +30,8 @@
} }
} }
.card-text { .card-text {
font-size: 22px; font-size: 20px;
margin-top: -9px; margin: auto;
position: relative; position: relative;
} }
} }
@@ -33,11 +39,14 @@
.difficulty-skeleton { .difficulty-skeleton {
display: flex; display: flex;
justify-content: space-between; flex-direction: row;
justify-content: space-around;
height: 50.5px;
@media (min-width: 376px) { @media (min-width: 376px) {
flex-direction: row; flex-direction: row;
} }
.item { .item {
min-width: 120px;
max-width: 150px; max-width: 150px;
margin: 0; margin: 0;
width: -webkit-fill-available; width: -webkit-fill-available;
@@ -65,7 +74,7 @@
width: 100%; width: 100%;
display: block; display: block;
&:first-child { &:first-child {
margin: 14px auto 0; margin: 10px auto 4px;
max-width: 80px; max-width: 80px;
} }
&:last-child { &:last-child {
@@ -109,7 +118,7 @@
} }
.loading-container { .loading-container {
min-height: 76px; min-height: 50.5px;
} }
.main-title { .main-title {
@@ -133,7 +142,7 @@
text-align: center; text-align: center;
flex-direction: column; flex-direction: column;
justify-content: space-around; justify-content: space-around;
padding: 24px 20px; padding: 20px;
} }
} }
@@ -151,4 +160,50 @@
.symbol { .symbol {
font-size: 13px; font-size: 13px;
}
.epoch-progress {
width: 100%;
height: 22px;
margin-bottom: 12px;
}
.epoch-blocks {
display: block;
width: 100%;
background: #2d3348;
.rect {
fill: #2d3348;
&.behind {
fill: #D81B60;
}
&.mined {
fill: url(#diff-gradient);
}
&.ahead {
fill: #1a9436;
}
&.hover {
fill: #535e84;
&.behind {
fill: #e94d86;
}
&.mined {
fill: url(#diff-hover-gradient);
}
&.ahead {
fill: #29d951;
}
}
}
}
.blocks-ahead {
color: #3bcc49;
}
.blocks-behind {
color: #D81B60;
} }

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { combineLatest, Observable, timer } from 'rxjs'; import { combineLatest, Observable, timer } from 'rxjs';
import { map, switchMap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { StateService } from '../..//services/state.service'; import { StateService } from '../..//services/state.service';
@@ -7,16 +7,33 @@ interface EpochProgress {
base: string; base: string;
change: number; change: number;
progress: number; progress: number;
minedBlocks: number;
remainingBlocks: number; remainingBlocks: number;
expectedBlocks: number;
newDifficultyHeight: number; newDifficultyHeight: number;
colorAdjustments: string; colorAdjustments: string;
colorPreviousAdjustments: string; colorPreviousAdjustments: string;
estimatedRetargetDate: number; estimatedRetargetDate: number;
retargetDateString: string;
previousRetarget: number; previousRetarget: number;
blocksUntilHalving: number; blocksUntilHalving: number;
timeUntilHalving: number; timeUntilHalving: number;
timeAvg: number;
} }
type BlockStatus = 'mined' | 'behind' | 'ahead' | 'next' | 'remaining';
interface DiffShape {
x: number;
y: number;
w: number;
h: number;
status: BlockStatus;
expected: boolean;
}
const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
@Component({ @Component({
selector: 'app-difficulty', selector: 'app-difficulty',
templateUrl: './difficulty.component.html', templateUrl: './difficulty.component.html',
@@ -24,15 +41,27 @@ interface EpochProgress {
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class DifficultyComponent implements OnInit { export class DifficultyComponent implements OnInit {
isLoadingWebSocket$: Observable<boolean>;
difficultyEpoch$: Observable<EpochProgress>;
@Input() showProgress = true; @Input() showProgress = true;
@Input() showHalving = false; @Input() showHalving = false;
@Input() showTitle = true; @Input() showTitle = true;
isLoadingWebSocket$: Observable<boolean>;
difficultyEpoch$: Observable<EpochProgress>;
epochStart: number;
currentHeight: number;
currentIndex: number;
expectedHeight: number;
expectedIndex: number;
difference: number;
shapes: DiffShape[];
tooltipPosition = { x: 0, y: 0 };
hoverSection: DiffShape | void;
constructor( constructor(
public stateService: StateService, public stateService: StateService,
@Inject(LOCALE_ID) private locale: string,
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
@@ -65,22 +94,110 @@ export class DifficultyComponent implements OnInit {
const blocksUntilHalving = 210000 - (block.height % 210000); const blocksUntilHalving = 210000 - (block.height % 210000);
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000); const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
const newEpochStart = Math.floor(this.stateService.latestBlockHeight / EPOCH_BLOCK_LENGTH) * EPOCH_BLOCK_LENGTH;
const newExpectedHeight = Math.floor(newEpochStart + da.expectedBlocks);
if (newEpochStart !== this.epochStart || newExpectedHeight !== this.expectedHeight || this.currentHeight !== this.stateService.latestBlockHeight) {
this.epochStart = newEpochStart;
this.expectedHeight = newExpectedHeight;
this.currentHeight = this.stateService.latestBlockHeight;
this.currentIndex = this.currentHeight - this.epochStart;
this.expectedIndex = Math.min(this.expectedHeight - this.epochStart, 2016) - 1;
this.difference = this.currentIndex - this.expectedIndex;
this.shapes = [];
this.shapes = this.shapes.concat(this.blocksToShapes(
0, Math.min(this.currentIndex, this.expectedIndex), 'mined', true
));
this.shapes = this.shapes.concat(this.blocksToShapes(
this.currentIndex + 1, this.expectedIndex, 'behind', true
));
this.shapes = this.shapes.concat(this.blocksToShapes(
this.expectedIndex + 1, this.currentIndex, 'ahead', false
));
if (this.currentIndex < 2015) {
this.shapes = this.shapes.concat(this.blocksToShapes(
this.currentIndex + 1, this.currentIndex + 1, 'next', (this.expectedIndex > this.currentIndex)
));
}
this.shapes = this.shapes.concat(this.blocksToShapes(
Math.max(this.currentIndex + 2, this.expectedIndex + 1), 2105, 'remaining', false
));
}
let retargetDateString;
if (da.remainingBlocks > 1870) {
retargetDateString = (new Date(da.estimatedRetargetDate)).toLocaleDateString(this.locale, { month: 'long', day: 'numeric' });
} else {
retargetDateString = (new Date(da.estimatedRetargetDate)).toLocaleTimeString(this.locale, { month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
}
const data = { const data = {
base: `${da.progressPercent.toFixed(2)}%`, base: `${da.progressPercent.toFixed(2)}%`,
change: da.difficultyChange, change: da.difficultyChange,
progress: da.progressPercent, progress: da.progressPercent,
remainingBlocks: da.remainingBlocks, minedBlocks: this.currentIndex + 1,
remainingBlocks: da.remainingBlocks - 1,
expectedBlocks: Math.floor(da.expectedBlocks),
colorAdjustments, colorAdjustments,
colorPreviousAdjustments, colorPreviousAdjustments,
newDifficultyHeight: da.nextRetargetHeight, newDifficultyHeight: da.nextRetargetHeight,
estimatedRetargetDate: da.estimatedRetargetDate, estimatedRetargetDate: da.estimatedRetargetDate,
retargetDateString,
previousRetarget: da.previousRetarget, previousRetarget: da.previousRetarget,
blocksUntilHalving, blocksUntilHalving,
timeUntilHalving, timeUntilHalving,
timeAvg: da.timeAvg,
}; };
return data; return data;
}) })
); );
} }
blocksToShapes(start: number, end: number, status: BlockStatus, expected: boolean = false): DiffShape[] {
const startY = start % 9;
const startX = Math.floor(start / 9);
const endY = (end % 9);
const endX = Math.floor(end / 9);
if (startX > endX) {
return [];
}
if (startX === endX) {
return [{
x: startX, y: startY, w: 1, h: 1 + endY - startY, status, expected
}];
}
const shapes = [];
shapes.push({
x: startX, y: startY, w: 1, h: 9 - startY, status, expected
});
shapes.push({
x: endX, y: 0, w: 1, h: endY + 1, status, expected
});
if (startX < endX - 1) {
shapes.push({
x: startX + 1, y: 0, w: endX - startX - 1, h: 9, status, expected
});
}
return shapes;
}
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
this.tooltipPosition = { x: event.clientX, y: event.clientY };
}
onHover(event, rect): void {
this.hoverSection = rect;
}
onBlur(event): void {
this.hoverSection = null;
}
} }

View File

@@ -1,10 +1,9 @@
<div *ngIf="stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING" class="mb-3 d-flex menu" <div *ngIf="stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING" class="mb-3 d-flex menu">
style="padding: 0px 35px;">
<a routerLinkActive="active" class="btn btn-primary mr-1" [class]="padding" <a routerLinkActive="active" class="btn btn-primary" [class]="padding"
[routerLink]="['/graphs/mempool' | relativeUrl]">Mempool</a> [routerLink]="['/graphs/mempool' | relativeUrl]">Mempool</a>
<div ngbDropdown class="mr-1" [class]="padding" *ngIf="stateService.env.MINING_DASHBOARD"> <div ngbDropdown [class]="padding" *ngIf="stateService.env.MINING_DASHBOARD">
<button class="btn btn-primary w-100" id="dropdownBasic1" ngbDropdownToggle i18n="mining">Mining</button> <button class="btn btn-primary w-100" id="dropdownBasic1" ngbDropdownToggle i18n="mining">Mining</button>
<div ngbDropdownMenu aria-labelledby="dropdownBasic1"> <div ngbDropdownMenu aria-labelledby="dropdownBasic1">
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/pools' | relativeUrl]" <a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/pools' | relativeUrl]"

View File

@@ -1,6 +1,15 @@
.menu { .menu {
flex-grow: 1; flex-grow: 1;
padding: 0 35px;
@media (min-width: 576px) { @media (min-width: 576px) {
max-width: 400px; max-width: 400px;
} }
}
& > * {
margin: 0;
margin-inline-end: 0.25rem;
&.last-child {
margin-inline-end: 0;
}
}
}

View File

@@ -54,31 +54,6 @@
height: 240px; height: 240px;
} }
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.pool-distribution { .pool-distribution {
min-height: 56px; min-height: 56px;
display: block; display: block;

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
import { EChartsOption, graphic } from 'echarts'; import { EChartsOption, graphic } from 'echarts';
import { Observable } from 'rxjs'; import { merge, Observable, of } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; import { map, mergeMap, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { formatNumber } from '@angular/common'; import { formatNumber } from '@angular/common';
@@ -84,77 +84,84 @@ export class HashrateChartComponent implements OnInit {
} }
}); });
this.hashrateObservable$ = this.radioGroupForm.get('dateSpan').valueChanges this.hashrateObservable$ = merge(
.pipe( this.radioGroupForm.get('dateSpan').valueChanges
startWith(this.radioGroupForm.controls.dateSpan.value), .pipe(
switchMap((timespan) => { startWith(this.radioGroupForm.controls.dateSpan.value),
if (!this.widget && !firstRun) { switchMap((timespan) => {
this.storageService.setValue('miningWindowPreference', timespan); if (!this.widget && !firstRun) {
} this.storageService.setValue('miningWindowPreference', timespan);
this.timespan = timespan; }
firstRun = false; this.timespan = timespan;
this.miningWindowPreference = timespan; firstRun = false;
this.isLoading = true; this.miningWindowPreference = timespan;
return this.apiService.getHistoricalHashrate$(timespan) this.isLoading = true;
.pipe( return this.apiService.getHistoricalHashrate$(this.timespan);
tap((response) => { })
const data = response.body; ),
this.stateService.chainTip$
.pipe(
switchMap(() => {
return this.apiService.getHistoricalHashrate$(this.timespan);
})
)
).pipe(
tap((response: any) => {
const data = response.body;
// We generate duplicated data point so the tooltip works nicely // We generate duplicated data point so the tooltip works nicely
const diffFixed = []; const diffFixed = [];
let diffIndex = 1; let diffIndex = 1;
let hashIndex = 0; let hashIndex = 0;
while (hashIndex < data.hashrates.length) { while (hashIndex < data.hashrates.length) {
if (diffIndex >= data.difficulty.length) { if (diffIndex >= data.difficulty.length) {
while (hashIndex < data.hashrates.length) { while (hashIndex < data.hashrates.length) {
diffFixed.push({ diffFixed.push({
timestamp: data.hashrates[hashIndex].timestamp, timestamp: data.hashrates[hashIndex].timestamp,
difficulty: data.difficulty.length > 0 ? data.difficulty[data.difficulty.length - 1].difficulty : null difficulty: data.difficulty.length > 0 ? data.difficulty[data.difficulty.length - 1].difficulty : null
});
++hashIndex;
}
break;
}
while (hashIndex < data.hashrates.length && diffIndex < data.difficulty.length &&
data.hashrates[hashIndex].timestamp <= data.difficulty[diffIndex].time
) {
diffFixed.push({
timestamp: data.hashrates[hashIndex].timestamp,
difficulty: data.difficulty[diffIndex - 1].difficulty
});
++hashIndex;
}
++diffIndex;
}
let maResolution = 15;
const hashrateMa = [];
for (let i = maResolution - 1; i < data.hashrates.length; ++i) {
let avg = 0;
for (let y = maResolution - 1; y >= 0; --y) {
avg += data.hashrates[i - y].avgHashrate;
}
avg /= maResolution;
hashrateMa.push([data.hashrates[i].timestamp * 1000, avg]);
}
this.prepareChartOptions({
hashrates: data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate]),
difficulty: diffFixed.map(val => [val.timestamp * 1000, val.difficulty]),
hashrateMa: hashrateMa,
}); });
this.isLoading = false; ++hashIndex;
}), }
map((response) => { break;
const data = response.body; }
return {
blockCount: parseInt(response.headers.get('x-total-count'), 10), while (hashIndex < data.hashrates.length && diffIndex < data.difficulty.length &&
currentDifficulty: data.currentDifficulty, data.hashrates[hashIndex].timestamp <= data.difficulty[diffIndex].time
currentHashrate: data.currentHashrate, ) {
}; diffFixed.push({
}), timestamp: data.hashrates[hashIndex].timestamp,
); difficulty: data.difficulty[diffIndex - 1].difficulty
});
++hashIndex;
}
++diffIndex;
}
let maResolution = 15;
const hashrateMa = [];
for (let i = maResolution - 1; i < data.hashrates.length; ++i) {
let avg = 0;
for (let y = maResolution - 1; y >= 0; --y) {
avg += data.hashrates[i - y].avgHashrate;
}
avg /= maResolution;
hashrateMa.push([data.hashrates[i].timestamp * 1000, avg]);
}
this.prepareChartOptions({
hashrates: data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate]),
difficulty: diffFixed.map(val => [val.timestamp * 1000, val.difficulty]),
hashrateMa: hashrateMa,
});
this.isLoading = false;
}),
map((response) => {
const data = response.body;
return {
blockCount: parseInt(response.headers.get('x-total-count'), 10),
currentDifficulty: data.currentDifficulty,
currentHashrate: data.currentHashrate,
};
}), }),
share() share()
); );

View File

@@ -48,31 +48,6 @@
max-height: 293px; max-height: 293px;
} }
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.loadingGraphs { .loadingGraphs {
position: absolute; position: absolute;
top: 50%; top: 50%;

View File

@@ -2,7 +2,7 @@
<div class="mempool-blocks-container" [class.time-ltr]="timeLtr" *ngIf="(difficultyAdjustments$ | async) as da;"> <div class="mempool-blocks-container" [class.time-ltr]="timeLtr" *ngIf="(difficultyAdjustments$ | async) as da;">
<div class="flashing"> <div class="flashing">
<ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks$ | async" let-i="index" [ngForTrackBy]="trackByFn"> <ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks$ | async" let-i="index" [ngForTrackBy]="trackByFn">
<div [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink"> <div @blockEntryTrigger [@.disabled]="!animateEntry" [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink">
<a draggable="false" [routerLink]="['/mempool-block/' | relativeUrl, i]" <a draggable="false" [routerLink]="['/mempool-block/' | relativeUrl, i]"
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}">&nbsp;</a> class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}">&nbsp;</a>
<div class="block-body"> <div class="block-body">
@@ -23,10 +23,10 @@
</div> </div>
<div [attr.data-cy]="'mempool-block-' + i + '-time'" class="time-difference" *ngIf="projectedBlock.blockVSize <= stateService.blockVSize; else mergedBlock"> <div [attr.data-cy]="'mempool-block-' + i + '-time'" class="time-difference" *ngIf="projectedBlock.blockVSize <= stateService.blockVSize; else mergedBlock">
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeDiffMainnet"> <ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeDiffMainnet">
<app-time-until [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true"></app-time-until> <app-time kind="until" [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true"></app-time>
</ng-template> </ng-template>
<ng-template #timeDiffMainnet> <ng-template #timeDiffMainnet>
<app-time-until [time]="da.timeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time-until> <app-time kind="until" [time]="da.timeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time>
</ng-template> </ng-template>
</div> </div>
<ng-template #mergedBlock> <ng-template #mergedBlock>

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input } from '@angular/core'; import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Subscription, Observable, fromEvent, merge, of, combineLatest, timer } from 'rxjs'; import { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs';
import { MempoolBlock } from '../../interfaces/websocket.interface'; import { MempoolBlock } from '../../interfaces/websocket.interface';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@@ -9,11 +9,18 @@ import { specialBlocks } from '../../app.constants';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { DifficultyAdjustment } from '../../interfaces/node-api.interface'; import { DifficultyAdjustment } from '../../interfaces/node-api.interface';
import { animate, style, transition, trigger } from '@angular/animations';
@Component({ @Component({
selector: 'app-mempool-blocks', selector: 'app-mempool-blocks',
templateUrl: './mempool-blocks.component.html', templateUrl: './mempool-blocks.component.html',
styleUrls: ['./mempool-blocks.component.scss'], styleUrls: ['./mempool-blocks.component.scss'],
animations: [trigger('blockEntryTrigger', [
transition(':enter', [
style({ transform: 'translateX(-155px)' }),
animate('2s 0s ease', style({ transform: '' })),
]),
])],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class MempoolBlocksComponent implements OnInit, OnDestroy { export class MempoolBlocksComponent implements OnInit, OnDestroy {
@@ -32,12 +39,14 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
isLoadingWebsocketSubscription: Subscription; isLoadingWebsocketSubscription: Subscription;
blockSubscription: Subscription; blockSubscription: Subscription;
networkSubscription: Subscription; networkSubscription: Subscription;
chainTipSubscription: Subscription;
network = ''; network = '';
now = new Date().getTime(); now = new Date().getTime();
timeOffset = 0; timeOffset = 0;
showMiningInfo = false; showMiningInfo = false;
timeLtrSubscription: Subscription; timeLtrSubscription: Subscription;
timeLtr: boolean; timeLtr: boolean;
animateEntry: boolean = false;
blockWidth = 125; blockWidth = 125;
blockPadding = 30; blockPadding = 30;
@@ -53,6 +62,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
resetTransitionTimeout: number; resetTransitionTimeout: number;
chainTip: number = -1;
blockIndex = 1; blockIndex = 1;
constructor( constructor(
@@ -69,6 +79,8 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
} }
ngOnInit() { ngOnInit() {
this.chainTip = this.stateService.latestBlockHeight;
if (['', 'testnet', 'signet'].includes(this.stateService.network)) { if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
this.enabledMiningInfoIfNeeded(this.location.path()); this.enabledMiningInfoIfNeeded(this.location.path());
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url)); this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
@@ -116,9 +128,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
mempoolBlocks.forEach((block, i) => { mempoolBlocks.forEach((block, i) => {
block.index = this.blockIndex + i; block.index = this.blockIndex + i;
block.height = lastBlock.height + i + 1; block.height = lastBlock.height + i + 1;
if (this.stateService.network === '') { block.blink = specialBlocks[block.height]?.networks.includes(this.stateService.network || 'mainnet') ? true : false;
block.blink = specialBlocks[block.height] ? true : false;
}
}); });
const stringifiedBlocks = JSON.stringify(mempoolBlocks); const stringifiedBlocks = JSON.stringify(mempoolBlocks);
@@ -155,11 +165,24 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
this.blockSubscription = this.stateService.blocks$ this.blockSubscription = this.stateService.blocks$
.subscribe(([block]) => { .subscribe(([block]) => {
if (block?.extras?.matchRate >= 66 && !this.tabHidden) { if (this.chainTip === -1) {
this.animateEntry = block.height === this.stateService.latestBlockHeight;
} else {
this.animateEntry = block.height > this.chainTip;
}
this.chainTip = this.stateService.latestBlockHeight;
if ((block?.extras?.similarity == null || block?.extras?.similarity > 0.5) && !this.tabHidden) {
this.blockIndex++; this.blockIndex++;
} }
}); });
this.chainTipSubscription = this.stateService.chainTip$.subscribe((height) => {
if (this.chainTip === -1) {
this.chainTip = height;
}
});
this.networkSubscription = this.stateService.networkChanged$ this.networkSubscription = this.stateService.networkChanged$
.subscribe((network) => this.network = network); .subscribe((network) => this.network = network);
@@ -195,11 +218,12 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
this.blockSubscription.unsubscribe(); this.blockSubscription.unsubscribe();
this.networkSubscription.unsubscribe(); this.networkSubscription.unsubscribe();
this.timeLtrSubscription.unsubscribe(); this.timeLtrSubscription.unsubscribe();
this.chainTipSubscription.unsubscribe();
clearTimeout(this.resetTransitionTimeout); clearTimeout(this.resetTransitionTimeout);
} }
trackByFn(index: number, block: MempoolBlock) { trackByFn(index: number, block: MempoolBlock) {
return block.index; return (block.isStack) ? 'stack' : block.index;
} }
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] { reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
@@ -214,6 +238,10 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
lastBlock.feeRange = lastBlock.feeRange.concat(block.feeRange); lastBlock.feeRange = lastBlock.feeRange.concat(block.feeRange);
lastBlock.feeRange.sort((a, b) => a - b); lastBlock.feeRange.sort((a, b) => a - b);
lastBlock.medianFee = this.median(lastBlock.feeRange); lastBlock.medianFee = this.median(lastBlock.feeRange);
lastBlock.totalFees += block.totalFees;
}
if (blocks.length) {
blocks[blocks.length - 1].isStack = blocks[blocks.length - 1].blockVSize > this.stateService.blockVSize;
} }
return blocks; return blocks;
} }
@@ -333,4 +361,4 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
} }
return emptyBlocks; return emptyBlocks;
} }
} }

View File

@@ -4,7 +4,6 @@
<div class="row row-cols-1 row-cols-md-2"> <div class="row row-cols-1 row-cols-md-2">
<!-- Temporary stuff here - Will be moved to a component once we have more useful data to show -->
<div class="col"> <div class="col">
<div class="main-title"> <div class="main-title">
<span [attr.data-cy]="'reward-stats'" i18n="mining.reward-stats">Reward stats</span>&nbsp; <span [attr.data-cy]="'reward-stats'" i18n="mining.reward-stats">Reward stats</span>&nbsp;
@@ -22,7 +21,7 @@
<!-- difficulty adjustment --> <!-- difficulty adjustment -->
<div class="col"> <div class="col">
<div class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div> <div class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
<app-difficulty [attr.data-cy]="'difficulty-adjustment'" [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty> <app-difficulty-mining [attr.data-cy]="'difficulty-adjustment'" [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty-mining>
</div> </div>
<!-- pool distribution --> <!-- pool distribution -->

View File

@@ -32,7 +32,7 @@
</div> </div>
<div class="card-header" *ngIf="!widget"> <div class="card-header" *ngIf="!widget">
<div class="d-flex d-md-block align-items-baseline"> <div class="d-flex d-md-table-cell align-items-baseline">
<span i18n="mining.pools">Pools Ranking</span> <span i18n="mining.pools">Pools Ranking</span>
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()"> <button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon> <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
@@ -87,19 +87,19 @@
<table *ngIf="widget === false" class="table table-borderless text-center pools-table"> <table *ngIf="widget === false" class="table table-borderless text-center pools-table">
<thead> <thead>
<tr> <tr>
<th class="d-none d-md-block" i18n="mining.rank">Rank</th> <th class="d-none d-md-table-cell" i18n="mining.rank">Rank</th>
<th class=""></th> <th class=""></th>
<th class="" i18n="mining.pool-name">Pool</th> <th class="" i18n="mining.pool-name">Pool</th>
<th class="" *ngIf="this.miningWindowPreference === '24h'" i18n="mining.hashrate">Hashrate</th> <th class="" *ngIf="this.miningWindowPreference === '24h'" i18n="mining.hashrate">Hashrate</th>
<th class="" i18n="master-page.blocks">Blocks</th> <th class="" i18n="master-page.blocks">Blocks</th>
<th *ngIf="auditAvailable" class="health text-right widget" i18n="latest-blocks.avg_health" <th *ngIf="auditAvailable" class="health text-right widget" i18n="latest-blocks.avg_health"
i18n-ngbTooltip="latest-blocks.avg_health" ngbTooltip="Avg Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Avg Health</th> i18n-ngbTooltip="latest-blocks.avg_health" ngbTooltip="Avg Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Avg Health</th>
<th class="d-none d-md-block" i18n="mining.empty-blocks">Empty blocks</th> <th class="d-none d-md-table-cell" i18n="mining.empty-blocks">Empty blocks</th>
</tr> </tr>
</thead> </thead>
<tbody [attr.data-cy]="'pools-table'" *ngIf="(miningStatsObservable$ | async) as miningStats"> <tbody [attr.data-cy]="'pools-table'" *ngIf="(miningStatsObservable$ | async) as miningStats">
<tr *ngFor="let pool of miningStats.pools"> <tr *ngFor="let pool of miningStats.pools">
<td class="d-none d-md-block">{{ pool.rank }}</td> <td class="d-none d-md-table-cell">{{ pool.rank }}</td>
<td class="text-right"> <td class="text-right">
<img width="25" height="25" src="{{ pool.logo }}" [alt]="pool.name + ' mining pool logo'" onError="this.src = '/resources/mining-pools/default.svg'"> <img width="25" height="25" src="{{ pool.logo }}" [alt]="pool.name + ' mining pool logo'" onError="this.src = '/resources/mining-pools/default.svg'">
</td> </td>
@@ -107,7 +107,7 @@
<td class="" *ngIf="this.miningWindowPreference === '24h'">{{ pool.lastEstimatedHashrate }} {{ <td class="" *ngIf="this.miningWindowPreference === '24h'">{{ pool.lastEstimatedHashrate }} {{
miningStats.miningUnits.hashrateUnit }}</td> miningStats.miningUnits.hashrateUnit }}</td>
<td class="d-flex justify-content-center"> <td class="d-flex justify-content-center">
{{ pool.blockCount }}<span class="d-none d-md-block">&nbsp;({{ pool.share }}%)</span> {{ pool.blockCount }}<span class="d-none d-md-table-cell">&nbsp;({{ pool.share }}%)</span>
</td> </td>
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> <td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<a <a
@@ -121,16 +121,16 @@
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span> <span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
</ng-template> </ng-template>
</td> </td>
<td class="d-none d-md-block">{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td> <td class="d-none d-md-table-cell">{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td>
</tr> </tr>
<tr style="border-top: 1px solid #555"> <tr style="border-top: 1px solid #555">
<td class="d-none d-md-block"></td> <td class="d-none d-md-table-cell"></td>
<td class="text-right"></td> <td class="text-right"></td>
<td class=""><b i18n="mining.all-miners">All miners</b></td> <td class=""><b i18n="mining.all-miners">All miners</b></td>
<td class="" *ngIf="this.miningWindowPreference === '24h'"><b>{{ miningStats.lastEstimatedHashrate}} {{ <td class="" *ngIf="this.miningWindowPreference === '24h'"><b>{{ miningStats.lastEstimatedHashrate}} {{
miningStats.miningUnits.hashrateUnit }}</b></td> miningStats.miningUnits.hashrateUnit }}</b></td>
<td class=""><b>{{ miningStats.blockCount }}</b></td> <td class=""><b>{{ miningStats.blockCount }}</b></td>
<td class="d-none d-md-block"><b>{{ miningStats.totalEmptyBlock }} ({{ miningStats.totalEmptyBlockRatio <td class="d-none d-md-table-cell"><b>{{ miningStats.totalEmptyBlock }} ({{ miningStats.totalEmptyBlockRatio
}}%)</b></td> }}%)</b></td>
</tr> </tr>
</tbody> </tbody>

View File

@@ -33,31 +33,6 @@
} }
} }
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.bottom-padding { .bottom-padding {
@media (max-width: 992px) { @media (max-width: 992px) {
padding-bottom: 65px padding-bottom: 65px

View File

@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit, HostBinding
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { EChartsOption, PieSeriesOption } from 'echarts'; import { EChartsOption, PieSeriesOption } from 'echarts';
import { concat, Observable } from 'rxjs'; import { merge, Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { StorageService } from '../..//services/storage.service'; import { StorageService } from '../..//services/storage.service';
@@ -73,7 +73,7 @@ export class PoolRankingComponent implements OnInit {
} }
}); });
this.miningStatsObservable$ = concat( this.miningStatsObservable$ = merge(
this.radioGroupForm.get('dateSpan').valueChanges this.radioGroupForm.get('dateSpan').valueChanges
.pipe( .pipe(
startWith(this.radioGroupForm.controls.dateSpan.value), // (trigger when the page loads) startWith(this.radioGroupForm.controls.dateSpan.value), // (trigger when the page loads)
@@ -89,7 +89,7 @@ export class PoolRankingComponent implements OnInit {
return this.miningService.getMiningStats(this.miningWindowPreference); return this.miningService.getMiningStats(this.miningWindowPreference);
}) })
), ),
this.stateService.blocks$ this.stateService.chainTip$
.pipe( .pipe(
switchMap(() => { switchMap(() => {
return this.miningService.getMiningStats(this.miningWindowPreference); return this.miningService.getMiningStats(this.miningWindowPreference);
@@ -162,10 +162,10 @@ export class PoolRankingComponent implements OnInit {
if (this.miningWindowPreference === '24h') { if (this.miningWindowPreference === '24h') {
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` + return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
pool.lastEstimatedHashrate.toString() + ' PH/s' + pool.lastEstimatedHashrate.toString() + ' PH/s' +
`<br>` + $localize`${i} blocks`; `<br>` + $localize`${ i }:INTERPOLATION: blocks`;
} else { } else {
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` + return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
$localize`${i} blocks`; $localize`${ i }:INTERPOLATION: blocks`;
} }
} }
}, },
@@ -195,13 +195,15 @@ export class PoolRankingComponent implements OnInit {
}, },
borderColor: '#000', borderColor: '#000',
formatter: () => { formatter: () => {
const percentage = totalShareOther.toFixed(2) + '%';
const i = totalBlockOther.toString();
if (this.miningWindowPreference === '24h') { if (this.miningWindowPreference === '24h') {
return `<b style="color: white">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` + return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
totalEstimatedHashrateOther.toString() + ' PH/s' + totalEstimatedHashrateOther.toString() + ' PH/s' +
`<br>` + totalBlockOther.toString() + ` blocks`; `<br>` + $localize`${ i }:INTERPOLATION: blocks`;
} else { } else {
return `<b style="color: white">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` + return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
totalBlockOther.toString() + ` blocks`; $localize`${ i }:INTERPOLATION: blocks`;
} }
} }
}, },

View File

@@ -86,11 +86,6 @@ export class PoolPreviewComponent implements OnInit {
regexes += regex + '", "'; regexes += regex + '", "';
} }
poolStats.pool.regexes = regexes.slice(0, -3); poolStats.pool.regexes = regexes.slice(0, -3);
poolStats.pool.addresses = poolStats.pool.addresses;
if (poolStats.reportedHashrate) {
poolStats.luck = poolStats.estimatedHashrate / poolStats.reportedHashrate * 100;
}
this.openGraphService.waitOver('pool-stats-' + this.slug); this.openGraphService.waitOver('pool-stats-' + this.slug);

View File

@@ -38,12 +38,12 @@
<tr *ngIf="!isMobile()" class="taller-row"> <tr *ngIf="!isMobile()" class="taller-row">
<td class="label addresses" i18n="mining.addresses">Addresses</td> <td class="label addresses" i18n="mining.addresses">Addresses</td>
<td *ngIf="poolStats.pool.addresses.length else nodata" style="padding-top: 25px"> <td *ngIf="poolStats.pool.addresses.length else nodata" style="padding-top: 25px">
<a [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]" class="first-address"> <a class="addresses-data" [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]">
{{ poolStats.pool.addresses[0] }} {{ poolStats.pool.addresses[0] }}
</a> </a>
<div> <div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="gfg"> <div #collapse="ngbCollapse" [(ngbCollapse)]="gfg">
<a *ngFor="let address of poolStats.pool.addresses | slice: 1" <a class="addresses-data" *ngFor="let address of poolStats.pool.addresses | slice: 1"
[routerLink]="['/address' | relativeUrl, address]">{{ [routerLink]="['/address' | relativeUrl, address]">{{
address }}<br></a> address }}<br></a>
</div> </div>
@@ -67,13 +67,13 @@
[attr.aria-expanded]="!gfg" aria-controls="collapseExample"> [attr.aria-expanded]="!gfg" aria-controls="collapseExample">
<span i18n="show-all">Show all</span> ({{ poolStats.pool.addresses.length }}) <span i18n="show-all">Show all</span> ({{ poolStats.pool.addresses.length }})
</button> </button>
<a [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]"> <a class="addresses-data" [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]">
{{ poolStats.pool.addresses[0] | shortenString: 40 }} {{ poolStats.pool.addresses[0] | shortenString: 30 }}
</a> </a>
<div #collapse="ngbCollapse" [(ngbCollapse)]="gfg" style="width: 100%"> <div #collapse="ngbCollapse" [(ngbCollapse)]="gfg" style="width: 100%">
<a *ngFor="let address of poolStats.pool.addresses | slice: 1" <a class="addresses-data" *ngFor="let address of poolStats.pool.addresses | slice: 1"
[routerLink]="['/address' | relativeUrl, address]">{{ [routerLink]="['/address' | relativeUrl, address]">{{
address | shortenString: 40 }}<br></a> address | shortenString: 30 }}<br></a>
</div> </div>
</div> </div>
</td> </td>
@@ -88,22 +88,25 @@
<!-- Hashrate desktop --> <!-- Hashrate desktop -->
<tr *ngIf="!isMobile()" class="taller-row"> <tr *ngIf="!isMobile()" class="taller-row">
<td class="label" i18n="mining.hashrate-24h">Hashrate (24h)</td>
<td class="data"> <td class="data">
<table class="table table-xs table-data"> <table class="table table-xs table-data">
<thead> <thead>
<tr> <tr>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.reward">Reward</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.hashrate">Hashrate (24h)</th>
<th scope="col" class="block-count-title" style="width: 26%" i18n="mining.luck">Luck</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="latest-blocks.avg_health" *ngIf="auditAvailable">Avg Health</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<td>{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td> <td class="text-center"><app-amount [satoshis]="poolStats.totalReward" digitsInfo="1.0-0" [noFiat]="true"></app-amount></td>
<ng-template *ngIf="poolStats.luck; else noreported"> <td class="text-center">{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td>
<td>{{ poolStats.reportedHashrate | amountShortener : 1 : 'H/s' }}</td> <td class="text-center" *ngIf="auditAvailable; else emptyTd"><span class="health-badge badge" [class.badge-success]="poolStats.avgBlockHealth >= 99"
<td>{{ formatNumber(poolStats.luck, this.locale, '1.2-2') }}%</td> [class.badge-warning]="poolStats.avgBlockHealth >= 75 && poolStats.avgBlockHealth < 99" [class.badge-danger]="poolStats.avgBlockHealth < 75"
</ng-template> *ngIf="poolStats.avgBlockHealth != null; else nullHealth">{{ poolStats.avgBlockHealth }}%</span>
<ng-template #nullHealth>
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
</ng-template>
</td>
</tbody> </tbody>
</table> </table>
</td> </td>
@@ -111,49 +114,46 @@
<!-- Hashrate mobile --> <!-- Hashrate mobile -->
<tr *ngIf="isMobile()"> <tr *ngIf="isMobile()">
<td colspan="2"> <td colspan="2">
<span class="label" i18n="mining.hashrate-24h">Hashrate (24h)</span>
<table class="table table-xs table-data"> <table class="table table-xs table-data">
<thead> <thead>
<tr> <tr>
<th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.reward">Reward</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.hashrate">Hashrate (24h)</th>
<th scope="col" class="block-count-title" style="width: 30%" i18n="mining.luck">Luck</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="latest-blocks.avg_health" *ngIf="auditAvailable">Avg Health</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<td>{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td> <td class="text-center"><app-amount [satoshis]="poolStats.totalReward" digitsInfo="1.0-0" [noFiat]="true"></app-amount></td>
<ng-template *ngIf="poolStats.luck; else noreported"> <td class="text-center">{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td>
<td>{{ poolStats.reportedHashrate | amountShortener : 1 : 'H/s' }}</td> <td *ngIf="auditAvailable; else emptyTd" class="text-center"><span class="health-badge badge" [class.badge-success]="poolStats.avgBlockHealth >= 99"
<td>{{ formatNumber(poolStats.luck, this.locale, '1.2-2') }}%</td> [class.badge-warning]="poolStats.avgBlockHealth >= 75 && poolStats.avgBlockHealth < 99" [class.badge-danger]="poolStats.avgBlockHealth < 75"
</ng-template> *ngIf="poolStats.avgBlockHealth != null; else nullHealth">{{ poolStats.avgBlockHealth }}%</span>
<ng-template #nullHealth>
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
</ng-template>
</td>
</tbody> </tbody>
</table> </table>
</td> </td>
</tr> </tr>
<ng-template #noreported>
<td>~</td>
<td>~</td>
</ng-template>
<!-- Mined blocks desktop --> <!-- Mined blocks desktop -->
<tr *ngIf="!isMobile()" class="taller-row"> <tr *ngIf="!isMobile()" class="taller-row">
<td class="label" i18n="mining.mined-blocks">Mined blocks</td>
<td class="data"> <td class="data">
<table class="table table-xs table-data"> <table class="table table-xs table-data">
<thead> <thead>
<tr> <tr>
<th scope="col" class="block-count-title" style="width: 37%" i18n="24h">24h</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="1w">1w</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="block-count-title" style="width: 26%" i18n="all">All</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<td>{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 * <td class="text-center">{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
poolStats.blockShare['24h'], this.locale, '1.0-0') }}%)</td> poolStats.blockShare['24h'], this.locale, '1.0-0') }}%)</td>
<td>{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 * <td class="text-center">{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
poolStats.blockShare['1w'], this.locale, '1.0-0') }}%)</td> poolStats.blockShare['1w'], this.locale, '1.0-0') }}%)</td>
<td>{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 * <td class="text-center">{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
poolStats.blockShare['all'], this.locale, '1.0-0') }}%)</td> poolStats.blockShare['all'], this.locale, '1.0-0') }}%)</td>
</tbody> </tbody>
</table> </table>
@@ -162,21 +162,20 @@
<!-- Mined blocks mobile --> <!-- Mined blocks mobile -->
<tr *ngIf="isMobile()"> <tr *ngIf="isMobile()">
<td colspan=2> <td colspan=2>
<span class="label" i18n="mining.mined-blocks">Mined blocks</span>
<table class="table table-xs table-data"> <table class="table table-xs table-data">
<thead> <thead>
<tr> <tr>
<th scope="col" class="block-count-title" style="width: 33%" i18n="24h">24h</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="1w">1w</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="block-count-title" style="width: 30%" i18n="all">All</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<td>{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 * <td class="text-center">{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
poolStats.blockShare['24h'], this.locale, '1.0-0') }}%)</td> poolStats.blockShare['24h'], this.locale, '1.0-0') }}%)</td>
<td>{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 * <td class="text-center">{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
poolStats.blockShare['1w'], this.locale, '1.0-0') }}%)</td> poolStats.blockShare['1w'], this.locale, '1.0-0') }}%)</td>
<td>{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 * <td class="text-center">{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
poolStats.blockShare['all'], this.locale, '1.0-0') }}%)</td> poolStats.blockShare['all'], this.locale, '1.0-0') }}%)</td>
</tbody> </tbody>
</table> </table>
@@ -213,8 +212,9 @@
<th class="timestamp" i18n="latest-blocks.timestamp">Timestamp</th> <th class="timestamp" i18n="latest-blocks.timestamp">Timestamp</th>
<th class="mined" i18n="latest-blocks.mined">Mined</th> <th class="mined" i18n="latest-blocks.mined">Mined</th>
<th class="coinbase text-left" i18n="latest-blocks.coinbasetag">Coinbase tag</th> <th class="coinbase text-left" i18n="latest-blocks.coinbasetag">Coinbase tag</th>
<th *ngIf="auditAvailable" class="health text-right" i18n="latest-blocks.health">Health</th>
<th class="reward text-right" i18n="latest-blocks.reward">Reward</th> <th class="reward text-right" i18n="latest-blocks.reward">Reward</th>
<th class="fees text-right" i18n="latest-blocks.fees">Fees</th> <th *ngIf="!auditAvailable" class="fees text-right" i18n="latest-blocks.fees">Fees</th>
<th class="txs text-right" i18n="dashboard.txs">TXs</th> <th class="txs text-right" i18n="dashboard.txs">TXs</th>
<th class="size" i18n="latest-blocks.size">Size</th> <th class="size" i18n="latest-blocks.size">Size</th>
</thead> </thead>
@@ -227,17 +227,31 @@
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} &lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
</td> </td>
<td class="mined"> <td class="mined">
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since> <app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time>
</td> </td>
<td class="coinbase"> <td class="coinbase">
<span class="badge badge-secondary scriptmessage longer"> <span class="badge badge-secondary scriptmessage longer">
{{ block.extras.coinbaseRaw | hex2ascii }} {{ block.extras.coinbaseRaw | hex2ascii }}
</span> </span>
</td> </td>
<td *ngIf="auditAvailable" class="health text-right">
<a
class="health-badge badge"
[class.badge-success]="block.extras.matchRate >= 99"
[class.badge-warning]="block.extras.matchRate >= 75 && block.extras.matchRate < 99"
[class.badge-danger]="block.extras.matchRate < 75"
[routerLink]="block.extras.matchRate != null ? ['/block/' | relativeUrl, block.id] : null"
[state]="{ data: { block: block } }"
*ngIf="block.extras.matchRate != null; else nullHealth"
>{{ block.extras.matchRate }}%</a>
<ng-template #nullHealth>
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
</ng-template>
</td>
<td class="reward text-right"> <td class="reward text-right">
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2" [noFiat]="true"></app-amount> <app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
</td> </td>
<td class="fees text-right"> <td *ngIf="!auditAvailable" class="fees text-right">
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2" [noFiat]="true"></app-amount> <app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
</td> </td>
<td class="txs text-right"> <td class="txs text-right">
@@ -364,24 +378,23 @@
<!-- Hashrate desktop --> <!-- Hashrate desktop -->
<tr *ngIf="!isMobile()" class="taller-row"> <tr *ngIf="!isMobile()" class="taller-row">
<td class="label" i18n="mining.hashrate-24h">Hashrate (24h)</td>
<td class="data"> <td class="data">
<table class="table table-xs table-data text-center"> <table class="table table-xs table-data text-center">
<thead> <thead>
<tr> <tr>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.total-reward">Reward</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.estimated">Hashrate (24h)</th>
<th scope="col" class="block-count-title" style="width: 26%" i18n="mining.luck">Luck</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.luck" *ngIf="auditAvailable">Avg Health</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<td> <td class="text-center">
<div class="skeleton-loader data"></div> <div class="skeleton-loader data"></div>
</td> </td>
<td> <td class="text-center">
<div class="skeleton-loader data"></div> <div class="skeleton-loader data"></div>
</td> </td>
<td> <td class="text-center" *ngIf="auditAvailable">
<div class="skeleton-loader data"></div> <div class="skeleton-loader data"></div>
</td> </td>
</tbody> </tbody>
@@ -391,23 +404,22 @@
<!-- Hashrate mobile --> <!-- Hashrate mobile -->
<tr *ngIf="isMobile()"> <tr *ngIf="isMobile()">
<td colspan="2"> <td colspan="2">
<span class="label" i18n="mining.hashrate-24h">Hashrate (24h)</span>
<table class="table table-xs table-data text-center"> <table class="table table-xs table-data text-center">
<thead> <thead>
<tr> <tr>
<th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.total-reward">Reward</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.estimated">Hashrate (24h)</th>
<th scope="col" class="block-count-title" style="width: 30%" i18n="mining.luck">Luck</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.luck" *ngIf="auditAvailable">Avg Health</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<td> <td class="text-center">
<div class="skeleton-loader data"></div> <div class="skeleton-loader data"></div>
</td> </td>
<td> <td class="text-center">
<div class="skeleton-loader data"></div> <div class="skeleton-loader data"></div>
</td> </td>
<td> <td class="text-center" *ngIf="auditAvailable">
<div class="skeleton-loader data"></div> <div class="skeleton-loader data"></div>
</td> </td>
</tbody> </tbody>
@@ -417,24 +429,23 @@
<!-- Mined blocks desktop --> <!-- Mined blocks desktop -->
<tr *ngIf="!isMobile()" class="taller-row"> <tr *ngIf="!isMobile()" class="taller-row">
<td class="label" i18n="mining.mined-blocks">Mined blocks</td>
<td class="data"> <td class="data">
<table class="table table-xs table-data text-center"> <table class="table table-xs table-data text-center">
<thead> <thead>
<tr> <tr>
<th scope="col" class="block-count-title" style="width: 37%">24h</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="block-count-title" style="width: 37%">1w</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="block-count-title" style="width: 26%" i18n="all">All</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<td> <td class="text-center">
<div class="skeleton-loader data"></div> <div class="skeleton-loader data"></div>
</td> </td>
<td> <td class="text-center">
<div class="skeleton-loader data"></div> <div class="skeleton-loader data"></div>
</td> </td>
<td> <td class="text-center">
<div class="skeleton-loader data"></div> <div class="skeleton-loader data"></div>
</td> </td>
</tbody> </tbody>
@@ -444,23 +455,22 @@
<!-- Mined blocks mobile --> <!-- Mined blocks mobile -->
<tr *ngIf="isMobile()"> <tr *ngIf="isMobile()">
<td colspan=2> <td colspan=2>
<span class="label" i18n="mining.mined-blocks">Mined blocks</span>
<table class="table table-xs table-data text-center"> <table class="table table-xs table-data text-center">
<thead> <thead>
<tr> <tr>
<th scope="col" class="block-count-title" style="width: 33%">24h</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="block-count-title" style="width: 37%">1w</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="block-count-title" style="width: 30%" i18n="all">All</th> <th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<td> <td class="text-center">
<div class="skeleton-loader data"></div> <div class="skeleton-loader data"></div>
</td> </td>
<td> <td class="text-center">
<div class="skeleton-loader data"></div> <div class="skeleton-loader data"></div>
</td> </td>
<td> <td class="text-center">
<div class="skeleton-loader data"></div> <div class="skeleton-loader data"></div>
</td> </td>
</tbody> </tbody>
@@ -475,4 +485,8 @@
</div> </div>
</div> </div>
</div> </div>
</ng-template>
<ng-template #emptyTd>
<td class="text-center"></td>
</ng-template> </ng-template>

View File

@@ -68,6 +68,11 @@ div.scrollable {
vertical-align: top; vertical-align: top;
padding-top: 25px; padding-top: 25px;
} }
.addresses-data {
vertical-align: top;
font-family: monospace;
font-size: 14px;
}
.data { .data {
text-align: right; text-align: right;
@@ -100,7 +105,7 @@ div.scrollable {
@media (max-width: 875px) { @media (max-width: 875px) {
padding-left: 50px; padding-left: 50px;
} }
@media (max-width: 650px) { @media (max-width: 685px) {
display: none; display: none;
} }
} }
@@ -118,7 +123,7 @@ div.scrollable {
padding-right: 10px; padding-right: 10px;
} }
@media (max-width: 875px) { @media (max-width: 875px) {
padding-right: 50px; padding-right: 20px;
} }
@media (max-width: 567px) { @media (max-width: 567px) {
padding-right: 10px; padding-right: 10px;
@@ -186,10 +191,6 @@ div.scrollable {
.block-count-title { .block-count-title {
color: #4a68b9; color: #4a68b9;
font-size: 14px; font-size: 14px;
text-align: left;
@media (max-width: 767.98px) {
text-align: center;
}
} }
.table-data tr { .table-data tr {

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { EChartsOption, graphic } from 'echarts'; import { EChartsOption, graphic } from 'echarts';
import { BehaviorSubject, Observable, timer } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; import { distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators';
import { BlockExtended, PoolStat } from '../../interfaces/node-api.interface'; import { BlockExtended, PoolStat } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
@@ -35,6 +35,8 @@ export class PoolComponent implements OnInit {
blocks: BlockExtended[] = []; blocks: BlockExtended[] = [];
slug: string = undefined; slug: string = undefined;
auditAvailable = false;
loadMoreSubject: BehaviorSubject<number> = new BehaviorSubject(this.blocks[this.blocks.length - 1]?.height); loadMoreSubject: BehaviorSubject<number> = new BehaviorSubject(this.blocks[this.blocks.length - 1]?.height);
constructor( constructor(
@@ -44,6 +46,7 @@ export class PoolComponent implements OnInit {
public stateService: StateService, public stateService: StateService,
private seoService: SeoService, private seoService: SeoService,
) { ) {
this.auditAvailable = this.stateService.env.AUDIT;
} }
ngOnInit(): void { ngOnInit(): void {
@@ -74,11 +77,6 @@ export class PoolComponent implements OnInit {
regexes += regex + '", "'; regexes += regex + '", "';
} }
poolStats.pool.regexes = regexes.slice(0, -3); poolStats.pool.regexes = regexes.slice(0, -3);
poolStats.pool.addresses = poolStats.pool.addresses;
if (poolStats.reportedHashrate) {
poolStats.luck = poolStats.estimatedHashrate / poolStats.reportedHashrate * 100;
}
return Object.assign({ return Object.assign({
logo: `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg' logo: `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'

View File

@@ -50,14 +50,14 @@
</div> </div>
</div> </div>
<div class="item"> <div class="item">
<h5 class="card-title" i18n="mining.rewards-per-tx">Reward Per Tx</h5> <h5 class="card-title" i18n="mining.fees-per-block">Avg Block Fees</h5>
<div class="card-text"> <div class="card-text">
<div class="skeleton-loader"></div> <div class="skeleton-loader"></div>
<div class="skeleton-loader"></div> <div class="skeleton-loader"></div>
</div> </div>
</div> </div>
<div class="item"> <div class="item">
<h5 class="card-title" i18n="mining.average-fee">Reward Per Tx</h5> <h5 class="card-title" i18n="mining.average-fee">Avg Tx Fee</h5>
<div class="card-text"> <div class="card-text">
<div class="skeleton-loader"></div> <div class="skeleton-loader"></div>
<div class="skeleton-loader"></div> <div class="skeleton-loader"></div>

View File

@@ -1,7 +1,7 @@
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate> <form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
<div class="d-flex"> <div class="d-flex">
<div class="search-box-container mr-2"> <div class="search-box-container mr-2">
<input (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem"> <input autofocus (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
<app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results> <app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
</div> </div>
<div> <div>

View File

@@ -53,3 +53,8 @@ form {
margin-top: 1px; margin-top: 1px;
margin-right: 2px; margin-right: 2px;
} }
input:focus {
box-shadow: none;
border-color: #1b1f2c;
}

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