Compare commits

...

226 Commits

Author SHA1 Message Date
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
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
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
200 changed files with 26444 additions and 15545 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

@@ -171,52 +171,58 @@ Helpful link: https://gist.github.com/System-Glitch/cb4e87bf1ae3fec9925725bb3ebe
Run bitcoind on regtest:
```
bitcoind -regtest -rpcport=8332
bitcoind -regtest
```
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):
```
bitcoin-cli -regtest -rpcport=8332 loadwallet test
bitcoin-cli -regtest loadwallet test
```
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):
```
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:
```
./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`:
```
./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
address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress)
address=$(bitcoin-cli -regtest getnewaddress)
bitcoin-cli -regtest generatetoaddress 101 $address
for i in {1..1000000}
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
```
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

View File

@@ -27,7 +27,8 @@
"AUDIT": false,
"ADVANCED_GBT_AUDIT": false,
"ADVANCED_GBT_MEMPOOL": false,
"CPFP_INDEXING": false
"CPFP_INDEXING": false,
"DISK_CACHE_BLOCK_INTERVAL": 6
},
"CORE_RPC": {
"HOST": "127.0.0.1",

View File

@@ -28,7 +28,8 @@
"ADVANCED_GBT_AUDIT": "__MEMPOOL_ADVANCED_GBT_AUDIT__",
"ADVANCED_GBT_MEMPOOL": "__MEMPOOL_ADVANCED_GBT_MEMPOOL__",
"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": "__DISK_CACHE_BLOCK_INTERVAL__"
},
"CORE_RPC": {
"HOST": "__CORE_RPC_HOST__",

View File

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

View File

@@ -42,6 +42,7 @@ describe('Mempool Backend Config', () => {
ADVANCED_GBT_MEMPOOL: false,
CPFP_INDEXING: false,
MAX_BLOCKS_BULK_QUERY: 0,
DISK_CACHE_BLOCK_INTERVAL: 6,
});
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
@@ -106,6 +107,13 @@ describe('Mempool Backend Config', () => {
BISQ_URL: 'https://bisq.markets/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 {
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) {
return { censored: [], added: [], fresh: [], score: 0 };
return { censored: [], added: [], fresh: [], score: 0, similarity: 1 };
}
const matches: string[] = []; // present in both mined block and template
@@ -16,6 +16,8 @@ class Audit {
const isCensored = {}; // missing, without excuse
const isDisplaced = {};
let displacedWeight = 0;
let matchedWeight = 0;
let projectedWeight = 0;
const inBlock = {};
const inTemplate = {};
@@ -38,11 +40,16 @@ class Audit {
isCensored[txid] = true;
}
displacedWeight += mempool[txid].weight;
} else {
matchedWeight += mempool[txid].weight;
}
projectedWeight += mempool[txid].weight;
inTemplate[txid] = true;
}
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
// 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 numMatches = matches.length - 1; // adjust for coinbase tx
const score = numMatches > 0 ? (numMatches / (numMatches + numCensored)) : 0;
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
return {
censored: Object.keys(isCensored),
added,
fresh,
score
score,
similarity,
};
}
}

View File

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

View File

@@ -143,7 +143,10 @@ class Blocks {
* @returns 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 {
txid: tx.txid,
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...)
* @param block
@@ -641,7 +651,7 @@ class Blocks {
if (this.newBlockCallbacks.length) {
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();
}

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 { NodeSocket } from '../repositories/NodesSocketsRepository';
import { isIP } from 'net';
@@ -164,6 +164,30 @@ export class Common {
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 {
switch (interval) {
case '24h': return '1 DAY';
@@ -175,6 +199,7 @@ export class Common {
case '1y': return '1 YEAR';
case '2y': return '2 YEAR';
case '3y': return '3 YEAR';
case '4y': return '4 YEAR';
default: return null;
}
}

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 58;
private static currentVersion = 59;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -510,6 +510,11 @@ class DatabaseMigration {
// We only run some migration queries for this version
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('ALTER TABLE pools AUTO_INCREMENT = 1');
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
}
}
private async $convertCompactCpfpTables(): Promise<void> {
try {

View File

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

View File

@@ -11,23 +11,33 @@ import { Common } from './common';
class DiskCache {
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_NAMES = config.MEMPOOL.CACHE_DIR + '/cache{number}.json';
private static CHUNK_FILES = 25;
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) {
return;
}
if (this.isWritingCache) {
logger.debug('Saving cache already in progress. Skipping.')
logger.debug('Saving cache already in progress. Skipping.');
return;
}
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;
const mempool = memPool.getMempool();
@@ -40,19 +50,48 @@ class DiskCache {
const chunkSize = Math.floor(mempoolArray.length / DiskCache.CHUNK_FILES);
await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({
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.FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
if (sync) {
fs.writeFileSync(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++) {
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');
this.isWritingCache = false;
} catch (e) {
@@ -61,8 +100,8 @@ class DiskCache {
}
}
wipeCache() {
logger.notice(`Wipping nodejs backend cache/cache*.json files`);
wipeCache(): void {
logger.notice(`Wiping nodejs backend cache/cache*.json files`);
try {
fs.unlinkSync(DiskCache.FILE_NAME);
} catch (e: any) {
@@ -83,7 +122,7 @@ class DiskCache {
}
}
loadMempoolCache() {
loadMempoolCache(): void {
if (!fs.existsSync(DiskCache.FILE_NAME)) {
return;
}
@@ -97,6 +136,10 @@ class DiskCache {
logger.notice('Disk cache contains an outdated schema version. Clearing it and skipping the cache loading.');
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) {
for (const tx of data.mempoolArray) {

View File

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

View File

@@ -31,6 +31,11 @@ class Mempool {
private mempoolProtection = 0;
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() {
setInterval(this.updateTxPerSecond.bind(this), 1000);
setInterval(this.deleteExpiredTransactions.bind(this), 20000);
@@ -128,6 +133,16 @@ class Mempool {
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) {
if (!this.mempoolCache[txid]) {
try {
@@ -142,7 +157,10 @@ class Mempool {
}
hasChange = true;
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));
}
}
@@ -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
if (this.mempoolProtection === 0
&& currentMempoolSize > 20000

View File

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

View File

@@ -13,6 +13,7 @@ import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
import PricesRepository from '../../repositories/PricesRepository';
import { bitcoinCoreApi } from '../bitcoin/bitcoin-api-factory';
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
import database from '../../database';
class Mining {
private blocksPriceIndexingRunning = false;
@@ -117,7 +118,7 @@ class Mining {
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
} catch (e) {
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;
@@ -141,11 +142,14 @@ class Mining {
const blockCount1w: number = await BlocksRepository.$blockCount(pool.id, '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;
try {
currentEstimatedHashrate = await bitcoinClient.getNetworkHashPs(totalBlock24h);
} 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 {
@@ -162,6 +166,8 @@ class Mining {
},
estimatedHashrate: currentEstimatedHashrate * (blockCount24h / totalBlock24h),
reportedHashrate: null,
avgBlockHealth: avgHealth,
totalReward: totalReward,
};
}
@@ -208,7 +214,7 @@ class Mining {
const startedAt = 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);
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);
hashrates.length = 0;
}
@@ -256,7 +262,7 @@ class Mining {
const weeksPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totalIndexed / totalWeekIndexed * 10000) / 100;
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;
indexedThisRun = 0;
loadingIndicators.setProgress('weekly-hashrate-indexing', progress, false);
@@ -268,14 +274,14 @@ class Mining {
}
this.lastWeeklyHashrateIndexingDate = new Date().getUTCDate();
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 {
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);
} catch (e) {
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;
}
}
@@ -308,7 +314,7 @@ class Mining {
const startedAt = 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);
while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
@@ -346,7 +352,7 @@ class Mining {
const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totalIndexed / totalDayIndexed * 10000) / 100;
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;
indexedThisRun = 0;
loadingIndicators.setProgress('daily-hashrate-indexing', progress);
@@ -373,14 +379,14 @@ class Mining {
this.lastHashrateIndexingDate = new Date().getUTCDate();
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 {
logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining);
}
loadingIndicators.setProgress('daily-hashrate-indexing', 100);
} catch (e) {
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;
}
}
@@ -446,13 +452,13 @@ class Mining {
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
if (elapsedSeconds > 5) {
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;
}
}
if (totalIndexed > 0) {
logger.notice(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining);
logger.info(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining);
} else {
logger.debug(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining);
}
@@ -499,7 +505,7 @@ class Mining {
if (blocksWithoutPrices.length > 200000) {
logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`;
}
logger.debug(logStr);
logger.debug(logStr, logger.tags.mining);
await BlocksRepository.$saveBlockPrices(blocksPrices);
blocksPrices.length = 0;
}
@@ -511,7 +517,7 @@ class Mining {
if (blocksWithoutPrices.length > 200000) {
logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`;
}
logger.debug(logStr);
logger.debug(logStr, logger.tags.mining);
await BlocksRepository.$saveBlockPrices(blocksPrices);
}
} catch (e) {
@@ -568,6 +574,7 @@ class Mining {
private getTimeRange(interval: string | null, scale = 1): number {
switch (interval) {
case '4y': return 43200 * scale; // 12h
case '3y': return 43200 * scale; // 12h
case '2y': 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[] {
return statistic.map((s) => {
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/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/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('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
@@ -54,6 +55,9 @@ class StatisticsRoutes {
case '3y':
result = await statisticsApi.$list3Y();
break;
case '4y':
result = await statisticsApi.$list4Y();
break;
default:
result = await statisticsApi.$list2H();
}

View File

@@ -1,8 +1,8 @@
import logger from '../logger';
import * as WebSocket from 'ws';
import {
BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock, MempoolBlockDelta,
OptimizedStatistic, ILoadingIndicators, IConversionRates
BlockExtended, TransactionExtended, WebsocketResponse,
OptimizedStatistic, ILoadingIndicators
} from '../mempool.interfaces';
import blocks from './blocks';
import memPool from './mempool';
@@ -20,6 +20,7 @@ import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository
import Audit from './audit';
import { deepClone } from '../utils/clone';
import priceUpdater from '../tasks/price-updater';
import { ApiPrice } from '../repositories/PricesRepository';
class WebsocketHandler {
private wss: WebSocket.Server | undefined;
@@ -193,7 +194,7 @@ class WebsocketHandler {
});
}
handleNewConversionRates(conversionRates: IConversionRates) {
handleNewConversionRates(conversionRates: ApiPrice) {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
}
@@ -214,7 +215,7 @@ class WebsocketHandler {
'mempoolInfo': memPool.getMempoolInfo(),
'vBytesPerSecond': memPool.getVBytesPerSecond(),
'blocks': _blocks,
'conversions': priceUpdater.latestPrices,
'conversions': priceUpdater.getLatestPrices(),
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
'transactions': memPool.getLatestTransactions(),
'backendInfo': backendInfo.getBackendInfo(),
@@ -431,7 +432,7 @@ class WebsocketHandler {
}
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 stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
@@ -463,8 +464,14 @@ class WebsocketHandler {
if (block.extras) {
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[] = [];

View File

@@ -33,6 +33,7 @@ interface IConfig {
ADVANCED_GBT_MEMPOOL: boolean;
CPFP_INDEXING: boolean;
MAX_BLOCKS_BULK_QUERY: number;
DISK_CACHE_BLOCK_INTERVAL: number;
};
ESPLORA: {
REST_API_URL: string;
@@ -155,6 +156,7 @@ const defaults: IConfig = {
'ADVANCED_GBT_MEMPOOL': false,
'CPFP_INDEXING': false,
'MAX_BLOCKS_BULK_QUERY': 0,
'DISK_CACHE_BLOCK_INTERVAL': 6,
},
'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000',

View File

@@ -38,6 +38,8 @@ import forensicsService from './tasks/lightning/forensics.service';
import priceUpdater from './tasks/price-updater';
import chainTips from './api/chain-tips';
import { AxiosError } from 'axios';
import v8 from 'v8';
import { formatBytes, getBytesUnit } from './utils/format';
class Server {
private wss: WebSocket.Server | undefined;
@@ -45,6 +47,11 @@ class Server {
private app: Application;
private currentBackendRetryInterval = 5;
private maxHeapSize: number = 0;
private heapLogInterval: number = 60;
private warnedHeapCritical: boolean = false;
private lastHeapLogTime: number | null = null;
constructor() {
this.app = express();
@@ -137,6 +144,8 @@ class Server {
this.runMainUpdateLoop();
}
setInterval(() => { this.healthCheck(); }, 2500);
if (config.BISQ.ENABLED) {
bisq.startBisqService();
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price));
@@ -255,6 +264,26 @@ class Server {
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())();

View File

@@ -76,13 +76,13 @@ class Indexer {
this.tasksRunning.push(task);
const lastestPriceId = await PricesRepository.$getLatestPriceId();
if (priceUpdater.historyInserted === false || lastestPriceId === null) {
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`);
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining);
setTimeout(() => {
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
this.runSingleTask('blocksPrices');
}, 10000);
} else {
logger.debug(`Blocks prices indexer will run now`);
logger.debug(`Blocks prices indexer will run now`, logger.tags.mining);
await mining.$indexBlockPrices();
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
}
@@ -112,7 +112,7 @@ class Indexer {
this.runIndexer = false;
this.indexerRunning = true;
logger.info(`Running mining indexer`);
logger.debug(`Running mining indexer`);
await this.checkAvailableCoreIndexes();
@@ -122,7 +122,7 @@ class Indexer {
const chainValid = await blocks.$generateBlockDatabase();
if (chainValid === false) {
// 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);
this.indexerRunning = false;
return;

View File

@@ -153,6 +153,7 @@ export interface BlockExtension {
feeRange: number[]; // fee rate percentiles
reward: number;
matchRate: number | null;
similarity?: number;
pool: {
id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
name: string;
@@ -293,7 +294,6 @@ interface RequiredParams {
}
export interface ILoadingIndicators { [name: string]: number; }
export interface IConversionRates { [currency: string]: number; }
export interface IBackendInfo {
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
*/
@@ -748,6 +797,7 @@ class BlocksRepository {
SELECT height
FROM compact_cpfp_clusters
WHERE height <= ? AND height >= ?
GROUP BY height
ORDER BY height DESC;
`, [currentBlockHeight, minHeight]);

View File

@@ -20,9 +20,9 @@ class DifficultyAdjustmentsRepository {
await DB.query(query, params);
} catch (e: any) {
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 {
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;
}
}
@@ -54,7 +54,7 @@ class DifficultyAdjustmentsRepository {
const [rows] = await DB.query(query);
return rows as IndexedDifficultyAdjustment[];
} 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;
}
}
@@ -83,7 +83,7 @@ class DifficultyAdjustmentsRepository {
const [rows] = await DB.query(query);
return rows as IndexedDifficultyAdjustment[];
} 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;
}
}
@@ -93,27 +93,27 @@ class DifficultyAdjustmentsRepository {
const [rows]: any[] = await DB.query(`SELECT height FROM difficulty_adjustments`);
return rows.map(block => block.height);
} 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;
}
}
public async $deleteAdjustementsFromHeight(height: number): Promise<void> {
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]);
} 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;
}
}
public async $deleteLastAdjustment(): Promise<void> {
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`);
} 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;
}
}

View File

@@ -25,7 +25,7 @@ class HashratesRepository {
try {
await DB.query(query);
} 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;
}
}
@@ -51,7 +51,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query);
return rows;
} 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;
}
}
@@ -78,7 +78,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query);
return rows;
} 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;
}
}
@@ -93,7 +93,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query);
return rows.map(row => row.timestamp);
} 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;
}
}
@@ -128,7 +128,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query);
return rows;
} 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;
}
}
@@ -158,7 +158,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query, [pool.id]);
boundaries = rows[0];
} 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
@@ -173,7 +173,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query, [boundaries.firstTimestamp, boundaries.lastTimestamp, pool.id]);
return rows;
} 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;
}
}
@@ -192,7 +192,7 @@ class HashratesRepository {
}
return rows[0]['number'];
} 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;
}
}
@@ -201,7 +201,7 @@ class HashratesRepository {
* Delete most recent data points for re-indexing
*/
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 {
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.lastWeeklyHashrateIndexingDate = null;
} 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.lastWeeklyHashrateIndexingDate = null;
} 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 logger from '../logger';
import { IConversionRates } from '../mempool.interfaces';
import priceUpdater from '../tasks/price-updater';
export interface ApiPrice {
@@ -13,6 +12,16 @@ export interface ApiPrice {
AUD: number,
JPY: number,
}
const ApiPriceFields = `
UNIX_TIMESTAMP(time) as time,
USD,
EUR,
GBP,
CAD,
CHF,
AUD,
JPY
`;
export interface ExchangeRates {
USDEUR: number,
@@ -39,7 +48,7 @@ export const MAX_PRICES = {
};
class PricesRepository {
public async $savePrices(time: number, prices: IConversionRates): Promise<void> {
public async $savePrices(time: number, prices: ApiPrice): Promise<void> {
if (prices.USD === -1) {
// 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
@@ -60,77 +69,115 @@ class PricesRepository {
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ? )`,
[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));
throw e;
}
}
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;
}
public async $getLatestPriceId(): Promise<number | null> {
const [oldestRow] = await DB.query(`SELECT id from prices WHERE USD != 0 ORDER BY time DESC LIMIT 1`);
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 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
const [oldestRow] = await DB.query(`
SELECT id
FROM prices
ORDER BY time DESC
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 rates[0];
return rates[0] as ApiPrice;
}
public async $getNearestHistoricalPrice(timestamp: number | undefined): Promise<Conversion | null> {
try {
const [rates]: any[] = await DB.query(`
SELECT *, UNIX_TIMESTAMP(time) AS time
const [rates] = await DB.query(`
SELECT ${ApiPriceFields}
FROM prices
WHERE UNIX_TIMESTAMP(time) < ?
ORDER BY time DESC
LIMIT 1`,
[timestamp]
);
if (!rates) {
if (!Array.isArray(rates)) {
throw Error(`Cannot get single historical price from the database`);
}
// 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 = {
USDEUR: Math.round(latestPrice.EUR / latestPrice.USD * 100) / 100,
USDGBP: Math.round(latestPrice.GBP / latestPrice.USD * 100) / 100,
USDCAD: Math.round(latestPrice.CAD / latestPrice.USD * 100) / 100,
USDCHF: Math.round(latestPrice.CHF / latestPrice.USD * 100) / 100,
USDAUD: Math.round(latestPrice.AUD / latestPrice.USD * 100) / 100,
USDJPY: Math.round(latestPrice.JPY / latestPrice.USD * 100) / 100,
USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
};
return {
prices: rates,
prices: rates as ApiPrice[],
exchangeRates: exchangeRates
};
} catch (e) {
@@ -141,28 +188,35 @@ class PricesRepository {
public async $getHistoricalPrices(): Promise<Conversion | null> {
try {
const [rates]: any[] = await DB.query(`
SELECT *, UNIX_TIMESTAMP(time) AS time
const [rates] = await DB.query(`
SELECT ${ApiPriceFields}
FROM prices
ORDER BY time DESC
`);
if (!rates) {
if (!Array.isArray(rates)) {
throw Error(`Cannot get average historical price from the database`);
}
// 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 = {
USDEUR: Math.round(latestPrice.EUR / latestPrice.USD * 100) / 100,
USDGBP: Math.round(latestPrice.GBP / latestPrice.USD * 100) / 100,
USDCAD: Math.round(latestPrice.CAD / latestPrice.USD * 100) / 100,
USDCHF: Math.round(latestPrice.CHF / latestPrice.USD * 100) / 100,
USDAUD: Math.round(latestPrice.AUD / latestPrice.USD * 100) / 100,
USDJPY: Math.round(latestPrice.JPY / latestPrice.USD * 100) / 100,
USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
};
return {
prices: rates,
prices: rates as ApiPrice[],
exchangeRates: exchangeRates
};
} catch (e) {

View File

@@ -411,7 +411,7 @@ class LightningStatsImporter {
}
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) {
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 {
lastRun: number = 0;
currentSha: string | undefined = undefined;
currentSha: string | null = null;
poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL;
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL;
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;
}
@@ -33,7 +35,7 @@ class PoolsUpdater {
try {
const githubSha = await this.fetchPoolsSha(); // Fetch pools-v2.json sha from github
if (githubSha === undefined) {
if (githubSha === null) {
return;
}
@@ -42,12 +44,12 @@ class PoolsUpdater {
}
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;
}
// 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
!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';
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);
} else {
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);
await DB.query('ROLLBACK;');
}
logger.notice('PoolsUpdater completed');
logger.info('PoolsUpdater completed');
} catch (e) {
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
*/
private async getShaFromDb(): Promise<string | undefined> {
private async getShaFromDb(): Promise<string | null> {
try {
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) {
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
*/
private async fetchPoolsSha(): Promise<string | undefined> {
private async fetchPoolsSha(): Promise<string | null> {
const response = await this.query(this.treeUrl);
if (response !== undefined) {
@@ -133,7 +135,7 @@ class PoolsUpdater {
}
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 urlHist: string = 'https://api-pub.bitfinex.com/v2/candles/trade:{GRANULARITY}:tBTC{CURRENCY}/hist';
constructor() {
}
public async $fetchPrice(currency): Promise<number> {
const response = await query(this.url + currency);
if (response && response['last_price']) {

View File

@@ -98,7 +98,7 @@ class KrakenApi implements PriceFeed {
}
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 config from '../config';
import logger from '../logger';
import { IConversionRates } from '../mempool.interfaces';
import PricesRepository, { MAX_PRICES } from '../repositories/PricesRepository';
import PricesRepository, { ApiPrice, MAX_PRICES } from '../repositories/PricesRepository';
import BitfinexApi from './price-feeds/bitfinex-api';
import BitflyerApi from './price-feeds/bitflyer-api';
import CoinbaseApi from './price-feeds/coinbase-api';
@@ -21,18 +20,18 @@ export interface PriceFeed {
}
export interface PriceHistory {
[timestamp: number]: IConversionRates;
[timestamp: number]: ApiPrice;
}
class PriceUpdater {
public historyInserted = false;
lastRun = 0;
lastHistoricalRun = 0;
running = false;
feeds: PriceFeed[] = [];
currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'];
latestPrices: IConversionRates;
private ratesChangedCallback: ((rates: IConversionRates) => void) | undefined;
private lastRun = 0;
private lastHistoricalRun = 0;
private running = false;
private feeds: PriceFeed[] = [];
private currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'];
private latestPrices: ApiPrice;
private ratesChangedCallback: ((rates: ApiPrice) => void) | undefined;
constructor() {
this.latestPrices = this.getEmptyPricesObj();
@@ -44,8 +43,13 @@ class PriceUpdater {
this.feeds.push(new GeminiApi());
}
public getEmptyPricesObj(): IConversionRates {
public getLatestPrices(): ApiPrice {
return this.latestPrices;
}
public getEmptyPricesObj(): ApiPrice {
return {
time: 0,
USD: -1,
EUR: -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;
}
@@ -69,6 +73,11 @@ class PriceUpdater {
}
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) {
return;
}
@@ -84,7 +93,7 @@ class PriceUpdater {
if (this.historyInserted === false && config.DATABASE.ENABLED === true) {
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);
}
@@ -156,6 +165,10 @@ class PriceUpdater {
}
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
// grouped[123456789]['USD'] = [1, 2, 3, 4];
const grouped: any = {};
const grouped = {};
for (const historicalEntry of historicalPrices) {
for (const time in historicalEntry) {
if (existingPriceTimes.includes(parseInt(time, 10))) {
@@ -249,7 +262,7 @@ class PriceUpdater {
// Average prices and insert everything into the db
let totalInserted = 0;
for (const time in grouped) {
const prices: IConversionRates = this.getEmptyPricesObj();
const prices: ApiPrice = this.getEmptyPricesObj();
for (const currency in grouped[time]) {
if (grouped[time][currency].length === 0) {
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

@@ -112,6 +112,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over
"ADVANCED_GBT_MEMPOOL": false,
"CPFP_INDEXING": false,
"MAX_BLOCKS_BULK_QUERY": 0,
"DISK_CACHE_BLOCK_INTERVAL": 6
},
```
@@ -143,6 +144,7 @@ Corresponding `docker-compose.yml` overrides:
MEMPOOL_ADVANCED_GBT_MEMPOOL: ""
MEMPOOL_CPFP_INDEXING: ""
MAX_BLOCKS_BULK_QUERY: ""
DISK_CACHE_BLOCK_INTERVAL: ""
...
```
@@ -432,3 +434,26 @@ Corresponding `docker-compose.yml` overrides:
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 ./
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 ./
USER 1000

View File

@@ -26,7 +26,8 @@
"ADVANCED_GBT_AUDIT": __MEMPOOL_ADVANCED_GBT_AUDIT__,
"ADVANCED_GBT_MEMPOOL": __MEMPOOL_ADVANCED_GBT_MEMPOOL__,
"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": __DISK_CACHE_BLOCK_INTERVAL__
},
"CORE_RPC": {
"HOST": "__CORE_RPC_HOST__",
@@ -107,5 +108,11 @@
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__",
"BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__",
"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_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
__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_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}
@@ -31,6 +30,7 @@ __MEMPOOL_ADVANCED_GBT_AUDIT__=${MEMPOOL_ADVANCED_GBT_AUDIT:=false}
__MEMPOOL_ADVANCED_GBT_MEMPOOL__=${MEMPOOL_ADVANCED_GBT_MEMPOOL:=false}
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
__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_HOST__=${CORE_RPC_HOST:=127.0.0.1}
@@ -112,6 +112,13 @@ __LND_REST_API_URL__=${LND_REST_API_URL:="https://localhost:8080"}
# CLN
__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__}"
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
@@ -135,7 +142,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_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_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_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
@@ -144,6 +150,7 @@ 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_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_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_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json
@@ -215,4 +222,11 @@ sed -i "s!__LND_REST_API_URL__!${__LND_REST_API_URL__}!g" mempool-config.json
# CLN
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

View File

@@ -3,6 +3,11 @@
#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
localhostIP="127.0.0.1"
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/pools.json
src/resources/mining-pools/*
src/resources/*.mp4
src/resources/**/*.mp4
src/resources/**/*.vtt
# environment config
mempool-frontend-config.json

View File

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

View File

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

View File

@@ -87,9 +87,9 @@ export const languages: Language[] = [
{ code: 'ar', name: 'العربية' }, // Arabic
// { code: 'bg', name: 'Български' }, // Bulgarian
// { code: 'bs', name: 'Bosanski' }, // Bosnian
{ code: 'ca', name: 'Català' }, // Catalan
// { code: 'ca', name: 'Català' }, // Catalan
{ code: 'cs', name: 'Čeština' }, // Czech
// { code: 'da', name: 'Dansk' }, // Danish
{ code: 'da', name: 'Dansk' }, // Danish
{ code: 'de', name: 'Deutsch' }, // German
// { code: 'et', name: 'Eesti' }, // Estonian
// { code: 'el', name: 'Ελληνικά' }, // Greek
@@ -136,13 +136,90 @@ export const languages: Language[] = [
];
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': {
labelEvent: 'Taproot 🌱 activation',
labelEventCompleted: 'Taproot 🌱 has been activated!',
networks: ['mainnet'],
},
'840000': {
labelEvent: 'Halving 🥳',
labelEvent: 'Bitcoin\'s 4th Halving',
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>
&lrm;{{ block.time | date:'yyyy-MM-dd HH:mm' }}
<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>
</td>
</tr>

View File

@@ -17,7 +17,7 @@
<tbody *ngIf="blocks.value; else loadingTmpl">
<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><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 class="d-none d-md-block">{{ block.txs.length }}</td>
</tr>

View File

@@ -35,7 +35,7 @@
<td>
&lrm;{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }}
<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>
</td>
</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>
</ng-template>
</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>
</tr>
</tbody>

View File

@@ -254,3 +254,30 @@ export function selectPowerOfTen(val: number): { divider: number, unit: string }
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>
</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">
<h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
@@ -209,7 +225,7 @@
<span>EmbassyOS</span>
</a>
<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>
</a>
<a href="https://github.com/bisq-network/bisq" target="_blank" title="Bisq">
@@ -268,6 +284,26 @@
<img class="image" src="/resources/profile/nunchuk.svg" />
<span>Nunchuk</span>
</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>
@@ -383,6 +419,12 @@
<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>
</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>

View File

@@ -11,6 +11,12 @@
line-height: 32px;
}
.image.not-rounded {
border-radius: 0;
width: 60px;
height: 60px;
}
.intro {
margin: 25px auto 30px;
width: 250px;
@@ -36,9 +42,11 @@
video {
width: 640px;
height: 360px;
max-width: 90%;
margin-top: 0;
@media (min-width: 768px) {
height: 360px;
}
}
.social-icons {
@@ -51,9 +59,13 @@
.enterprise-sponsor,
.community-integrations-sponsor,
.maintainers {
margin-top: 68px;
margin-top: 30px;
margin-bottom: 68px;
scroll-margin: 30px;
@media (min-width: 768px) {
margin-top: 68px;
}
}
.maintainers {
@@ -199,6 +211,16 @@
a {
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 {
max-width: 965px;
max-width: 1110px;
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())
);
}
ngAfterViewInit() {
this.goToAnchor();
}
@@ -90,4 +90,8 @@ export class AboutComponent implements OnInit {
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">
<span class="fiat" *ngIf="blockConversion; else noblockconversion">
{{ addPlus && satoshis >= 0 ? '+' : '' }}
{{
{{ addPlus && satoshis >= 0 ? '+' : '' }}{{
(
(blockConversion.price[currency] >= 0 ? blockConversion.price[currency] : null) ??
(blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency]) ?? 0
(blockConversion.price[currency] > -1 ? blockConversion.price[currency] : null) ??
(blockConversion.price['USD'] > -1 ? blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency] : null) ?? 0
) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency
}}
</span>
<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-container>

View File

@@ -54,31 +54,6 @@
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 {
pointer-events: none;
opacity: 0.5;

View File

@@ -54,31 +54,6 @@
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 {
pointer-events: none;
opacity: 0.5;

View File

@@ -54,31 +54,6 @@
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 {
pointer-events: none;
opacity: 0.5;

View File

@@ -54,31 +54,6 @@
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 {
pointer-events: none;
opacity: 0.5;

View File

@@ -121,7 +121,7 @@
<ng-container *ngIf="!isLoadingBlock; else loadingRest">
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<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 *ngIf="block?.extras?.medianFee != undefined">
<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"
class="text-center bitcoin-block mined-block blockchain-blocks-offset-{{ offset }}-index-{{ 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 } }"
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}">&nbsp;</a>
<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>
</div>
<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 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"

View File

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

View File

@@ -26,7 +26,7 @@
</thead>
<tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<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>
</td>
<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' }}
</td>
<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 *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<a
@@ -89,7 +89,6 @@
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<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>
</td>
<td class="timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
@@ -98,7 +97,7 @@
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
<span class="skeleton-loader" style="max-width: 125px"></span>
</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>
</td>
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">

View File

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

View File

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

View File

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

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-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-until [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time-until></div>
<div class="epoch-progress">
<svg class="epoch-blocks" height="22px" width="100%" viewBox="0 0 224 9" shape-rendering="crispEdges" preserveAspectRatio="none">
<defs>
<linearGradient id="diff-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#105fb0" />
<stop offset="100%" stop-color="#9339f4" />
</linearGradient>
<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 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 class="difficulty-stats">
<div class="item">
<div class="card-text">
~<app-time [time]="epochData.timeAvg / 1000" [forceFloorOnTimeIntervals]="['minute']" [fractionDigits]="1"></app-time>
</div>
<div class="symbol" i18n="difficulty-box.average-block-time">Average block time</div>
</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>
<div class="item">
<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 #arrowDownPreviousDifficulty >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
<ng-template #arrowDownDifficulty >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
</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 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 class="item">
<div class="card-text"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time></div>
<div class="symbol">
{{ epochData.retargetDateString }}
</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>
<ng-template #loadingDifficulty>
<div class="epoch-progress">
<div class="skeleton-loader"></div>
</div>
<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.current-period">Current Period</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
@@ -85,3 +104,10 @@
</div>
</div>
</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 {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.difficulty-stats {
display: flex;
flex-direction: row;
justify-content: space-around;
height: 76px;
height: 50.5px;
.shared-block {
color: #ffffff66;
font-size: 12px;
@@ -24,8 +30,8 @@
}
}
.card-text {
font-size: 22px;
margin-top: -9px;
font-size: 20px;
margin: auto;
position: relative;
}
}
@@ -33,11 +39,14 @@
.difficulty-skeleton {
display: flex;
justify-content: space-between;
flex-direction: row;
justify-content: space-around;
height: 50.5px;
@media (min-width: 376px) {
flex-direction: row;
}
.item {
min-width: 120px;
max-width: 150px;
margin: 0;
width: -webkit-fill-available;
@@ -65,7 +74,7 @@
width: 100%;
display: block;
&:first-child {
margin: 14px auto 0;
margin: 10px auto 4px;
max-width: 80px;
}
&:last-child {
@@ -109,7 +118,7 @@
}
.loading-container {
min-height: 76px;
min-height: 50.5px;
}
.main-title {
@@ -133,7 +142,7 @@
text-align: center;
flex-direction: column;
justify-content: space-around;
padding: 24px 20px;
padding: 20px;
}
}
@@ -151,4 +160,50 @@
.symbol {
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 { map, switchMap } from 'rxjs/operators';
import { StateService } from '../..//services/state.service';
@@ -7,16 +7,33 @@ interface EpochProgress {
base: string;
change: number;
progress: number;
minedBlocks: number;
remainingBlocks: number;
expectedBlocks: number;
newDifficultyHeight: number;
colorAdjustments: string;
colorPreviousAdjustments: string;
estimatedRetargetDate: number;
retargetDateString: string;
previousRetarget: number;
blocksUntilHalving: 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({
selector: 'app-difficulty',
templateUrl: './difficulty.component.html',
@@ -24,15 +41,27 @@ interface EpochProgress {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DifficultyComponent implements OnInit {
isLoadingWebSocket$: Observable<boolean>;
difficultyEpoch$: Observable<EpochProgress>;
@Input() showProgress = true;
@Input() showHalving = false;
@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(
public stateService: StateService,
@Inject(LOCALE_ID) private locale: string,
) { }
ngOnInit(): void {
@@ -65,22 +94,110 @@ export class DifficultyComponent implements OnInit {
const blocksUntilHalving = 210000 - (block.height % 210000);
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 = {
base: `${da.progressPercent.toFixed(2)}%`,
change: da.difficultyChange,
progress: da.progressPercent,
remainingBlocks: da.remainingBlocks,
minedBlocks: this.currentIndex + 1,
remainingBlocks: da.remainingBlocks - 1,
expectedBlocks: Math.floor(da.expectedBlocks),
colorAdjustments,
colorPreviousAdjustments,
newDifficultyHeight: da.nextRetargetHeight,
estimatedRetargetDate: da.estimatedRetargetDate,
retargetDateString,
previousRetarget: da.previousRetarget,
blocksUntilHalving,
timeUntilHalving,
timeAvg: da.timeAvg,
};
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"
style="padding: 0px 35px;">
<div *ngIf="stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING" class="mb-3 d-flex menu">
<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>
<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>
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/pools' | relativeUrl]"

View File

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

View File

@@ -54,31 +54,6 @@
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 {
min-height: 56px;
display: block;

View File

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

View File

@@ -48,31 +48,6 @@
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 {
position: absolute;
top: 50%;

View File

@@ -2,7 +2,7 @@
<div class="mempool-blocks-container" [class.time-ltr]="timeLtr" *ngIf="(difficultyAdjustments$ | async) as da;">
<div class="flashing">
<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]"
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}">&nbsp;</a>
<div class="block-body">
@@ -23,10 +23,10 @@
</div>
<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">
<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 #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>
</div>
<ng-template #mergedBlock>

View File

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

View File

@@ -4,7 +4,6 @@
<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="main-title">
<span [attr.data-cy]="'reward-stats'" i18n="mining.reward-stats">Reward stats</span>&nbsp;
@@ -22,7 +21,7 @@
<!-- difficulty adjustment -->
<div class="col">
<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>
<!-- pool distribution -->

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 {
@media (max-width: 992px) {
padding-bottom: 65px

View File

@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit, HostBinding
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
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 { SeoService } from '../../services/seo.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
.pipe(
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);
})
),
this.stateService.blocks$
this.stateService.chainTip$
.pipe(
switchMap(() => {
return this.miningService.getMiningStats(this.miningWindowPreference);
@@ -162,10 +162,10 @@ export class PoolRankingComponent implements OnInit {
if (this.miningWindowPreference === '24h') {
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
pool.lastEstimatedHashrate.toString() + ' PH/s' +
`<br>` + $localize`${i} blocks`;
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
} else {
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',
formatter: () => {
const percentage = totalShareOther.toFixed(2) + '%';
const i = totalBlockOther.toString();
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' +
`<br>` + totalBlockOther.toString() + ` blocks`;
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
} else {
return `<b style="color: white">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` +
totalBlockOther.toString() + ` blocks`;
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
$localize`${ i }:INTERPOLATION: blocks`;
}
}
},

View File

@@ -86,11 +86,6 @@ export class PoolPreviewComponent implements OnInit {
regexes += regex + '", "';
}
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);

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
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 { BlockExtended, PoolStat } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
@@ -35,6 +35,8 @@ export class PoolComponent implements OnInit {
blocks: BlockExtended[] = [];
slug: string = undefined;
auditAvailable = false;
loadMoreSubject: BehaviorSubject<number> = new BehaviorSubject(this.blocks[this.blocks.length - 1]?.height);
constructor(
@@ -44,6 +46,7 @@ export class PoolComponent implements OnInit {
public stateService: StateService,
private seoService: SeoService,
) {
this.auditAvailable = this.stateService.env.AUDIT;
}
ngOnInit(): void {
@@ -74,11 +77,6 @@ export class PoolComponent implements OnInit {
regexes += regex + '", "';
}
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({
logo: `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'

View File

@@ -1,7 +1,7 @@
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
<div class="d-flex">
<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>
</div>
<div>

View File

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

View File

@@ -85,21 +85,20 @@ export class StartComponent implements OnInit, OnDestroy {
});
this.stateService.blocks$
.subscribe((blocks: any) => {
if (this.stateService.network !== '') {
return;
}
this.countdown = 0;
const block = blocks[0];
for (const sb in specialBlocks) {
const height = parseInt(sb, 10);
const diff = height - block.height;
if (diff > 0 && diff <= 1008) {
this.countdown = diff;
this.eventName = specialBlocks[sb].labelEvent;
if (specialBlocks[sb].networks.includes(this.stateService.network || 'mainnet')) {
const height = parseInt(sb, 10);
const diff = height - block.height;
if (diff > 0 && diff <= 1008) {
this.countdown = diff;
this.eventName = specialBlocks[sb].labelEvent;
}
}
}
if (specialBlocks[block.height]) {
if (specialBlocks[block.height] && specialBlocks[block.height].networks.includes(this.stateService.network || 'mainnet')) {
this.specialEvent = true;
this.eventName = specialBlocks[block.height].labelEventCompleted;
setTimeout(() => {

View File

@@ -49,6 +49,9 @@
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
<input type="radio" [value]="'3y'" [routerLink]="['/graphs' | relativeUrl]" fragment="3y" formControlName="dateSpan"> 3Y
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '4y'">
<input type="radio" [value]="'4y'" [routerLink]="['/graphs' | relativeUrl]" fragment="4y" formControlName="dateSpan"> 4Y
</label>
</div>
<div class="small-buttons">
<div ngbDropdown #myDrop="ngbDropdown">

View File

@@ -70,7 +70,7 @@ export class StatisticsComponent implements OnInit {
this.route
.fragment
.subscribe((fragment) => {
if (['2h', '24h', '1w', '1m', '3m', '6m', '1y', '2y', '3y'].indexOf(fragment) > -1) {
if (['2h', '24h', '1w', '1m', '3m', '6m', '1y', '2y', '3y', '4y'].indexOf(fragment) > -1) {
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
}
});
@@ -109,7 +109,10 @@ export class StatisticsComponent implements OnInit {
if (this.radioGroupForm.controls.dateSpan.value === '2y') {
return this.apiService.list2YStatistics$();
}
return this.apiService.list3YStatistics$();
if (this.radioGroupForm.controls.dateSpan.value === '3y') {
return this.apiService.list3YStatistics$();
}
return this.apiService.list4YStatistics$();
})
)
.subscribe((mempoolStats: any) => {
@@ -181,7 +184,7 @@ export class StatisticsComponent implements OnInit {
}
let capRatio = 10;
if (['1m', '3m', '6m', '1y', '2y', '3y'].includes(this.graphWindowPreference)) {
if (['1m', '3m', '6m', '1y', '2y', '3y', '4y'].includes(this.graphWindowPreference)) {
capRatio = 4;
}

View File

@@ -25,6 +25,12 @@
</defs>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'warning'">
<svg [class]="class" [style]="style" [attr.width]="width" [attr.height]="height" [attr.viewBox]="viewBox" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M135.3 34.474c-15.62 27.306-54.206 95.63-85.21 150.534L9.075 257.583C5.382 264.08 6.76 269.217 7.908 271.7c2.326 5.028 7.29 7.537 11.155 8.215l.78.133 264.698.006-.554-.02c4.152.255 9.664-1.24 12.677-6.194 1.926-3.18 3.31-8.589-1.073-16.278L213.637 114.37l-45.351-79.205c-5.681-9.932-12.272-12.022-16.8-12.022-4.42 0-10.818 1.964-16.181 11.331h-.006zm-69.072 159.94c30.997-54.885 69.563-123.184 85.16-150.446l.186-.297c.2.303.393.582.618.981l45.363 79.22s72.377 126.47 78.569 137.283l-247.618-.007 37.719-66.734" style="fill:#ffc107;fill-opacity:1"/>
<path d="M152.597 247.445c8.02 0 14.518-6.728 14.518-15.025 0-8.29-6.499-15.018-14.518-15.018-8.031 0-14.529 6.728-14.529 15.018 0 8.297 6.498 15.025 14.53 15.025m-.001-147.18c11.586 0 22.23 10.958 20.977 21.7l-9.922 75.564c-.966 6.601-4.95 11.433-11.055 11.433s-10.102-4.832-11.056-11.433l-9.927-75.564c-1.26-10.742 9.39-21.7 20.983-21.7" style="fill:#ffc107;fill-opacity:1"/>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'mempoolSpace'">
<svg [class]="class" [style]="style" [attr.width]="width" [attr.height]="height" [attr.viewBox]="viewBox" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M 219.548 86.198 L 219.548 63.833 C 219.548 60.359 218.746 57.686 217.163 55.919 C 215.601 54.151 213.237 53.267 210.195 53.267 C 206.762 53.267 203.946 54.377 202.013 56.453 C 200.081 58.55 199.053 61.633 199.053 65.395 L 199.053 86.219 L 191.447 86.219 L 191.447 63.833 C 191.447 56.823 188.282 53.267 182.032 53.267 C 178.6 53.267 175.783 54.377 173.851 56.453 C 171.919 58.55 170.891 61.633 170.891 65.395 L 170.891 86.219 L 163.285 86.219 L 163.285 46.422 L 170.685 46.422 L 170.685 50.759 C 173.687 47.799 178.003 46.175 182.999 46.175 C 188.96 46.175 193.667 48.498 196.36 52.753 C 199.608 48.559 204.85 46.175 210.955 46.175 C 215.93 46.175 219.877 47.614 222.693 50.43 C 225.632 53.39 227.174 57.871 227.154 63.36 L 227.154 86.198 L 219.548 86.198 Z" fill="white"/>
@@ -104,4 +110,4 @@
</g>
</g>
</svg>
</ng-template>
</ng-template>

View File

@@ -13,4 +13,5 @@ export class SvgImagesComponent {
@Input() width: string;
@Input() height: string;
@Input() viewBox: string;
@Input() fill: string;
}

View File

@@ -1,98 +0,0 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core';
import { StateService } from '../../services/state.service';
import { dates } from '../../shared/i18n/dates';
@Component({
selector: 'app-time-since',
template: `{{ text }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimeSinceComponent implements OnInit, OnChanges, OnDestroy {
interval: number;
text: string;
intervals = {};
@Input() time: number;
@Input() dateString: number;
@Input() fastRender = false;
constructor(
private ref: ChangeDetectorRef,
private stateService: StateService,
) {
this.intervals = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1
};
}
ngOnInit() {
if (!this.stateService.isBrowser) {
this.text = this.calculate();
this.ref.markForCheck();
return;
}
this.interval = window.setInterval(() => {
this.text = this.calculate();
this.ref.markForCheck();
}, 1000 * (this.fastRender ? 1 : 60));
}
ngOnChanges() {
this.text = this.calculate();
this.ref.markForCheck();
}
ngOnDestroy() {
clearInterval(this.interval);
}
calculate() {
let date: Date;
if (this.dateString) {
date = new Date(this.dateString)
} else {
date = new Date(this.time * 1000);
}
const seconds = Math.floor((+new Date() - +date) / 1000);
if (seconds < 60) {
return $localize`:@@date-base.just-now:Just now`;
}
let counter: number;
for (const i in this.intervals) {
if (this.intervals.hasOwnProperty(i)) {
counter = Math.floor(seconds / this.intervals[i]);
const dateStrings = dates(counter);
if (counter > 0) {
if (counter === 1) {
switch (i) { // singular (1 day)
case 'year': return $localize`:@@time-since:${dateStrings.i18nYear}:DATE: ago`; break;
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonth}:DATE: ago`; break;
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeek}:DATE: ago`; break;
case 'day': return $localize`:@@time-since:${dateStrings.i18nDay}:DATE: ago`; break;
case 'hour': return $localize`:@@time-since:${dateStrings.i18nHour}:DATE: ago`; break;
case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinute}:DATE: ago`; break;
case 'second': return $localize`:@@time-since:${dateStrings.i18nSecond}:DATE: ago`; break;
}
} else {
switch (i) { // plural (2 days)
case 'year': return $localize`:@@time-since:${dateStrings.i18nYears}:DATE: ago`; break;
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonths}:DATE: ago`; break;
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeeks}:DATE: ago`; break;
case 'day': return $localize`:@@time-since:${dateStrings.i18nDays}:DATE: ago`; break;
case 'hour': return $localize`:@@time-since:${dateStrings.i18nHours}:DATE: ago`; break;
case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinutes}:DATE: ago`; break;
case 'second': return $localize`:@@time-since:${dateStrings.i18nSeconds}:DATE: ago`; break;
}
}
}
}
}
}
}

View File

@@ -1,91 +0,0 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core';
import { StateService } from '../../services/state.service';
import { dates } from '../../shared/i18n/dates';
@Component({
selector: 'app-time-span',
template: `{{ text }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimeSpanComponent implements OnInit, OnChanges, OnDestroy {
interval: number;
text: string;
intervals = {};
@Input() time: number;
@Input() fastRender = false;
constructor(
private ref: ChangeDetectorRef,
private stateService: StateService,
) {
this.intervals = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1
};
}
ngOnInit() {
if (!this.stateService.isBrowser) {
this.text = this.calculate();
this.ref.markForCheck();
return;
}
this.interval = window.setInterval(() => {
this.text = this.calculate();
this.ref.markForCheck();
}, 1000 * (this.fastRender ? 1 : 60));
}
ngOnChanges() {
this.text = this.calculate();
this.ref.markForCheck();
}
ngOnDestroy() {
clearInterval(this.interval);
}
calculate() {
const seconds = Math.floor(this.time);
if (seconds < 60) {
return $localize`:@@date-base.just-now:Just now`;
}
let counter: number;
for (const i in this.intervals) {
if (this.intervals.hasOwnProperty(i)) {
counter = Math.floor(seconds / this.intervals[i]);
const dateStrings = dates(counter);
if (counter > 0) {
if (counter === 1) {
switch (i) { // singular (1 day)
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYear}:DATE:`; break;
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonth}:DATE:`; break;
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeek}:DATE:`; break;
case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDay}:DATE:`; break;
case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHour}:DATE:`; break;
case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinute}:DATE:`; break;
case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSecond}:DATE:`; break;
}
} else {
switch (i) { // plural (2 days)
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYears}:DATE:`; break;
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonths}:DATE:`; break;
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeeks}:DATE:`; break;
case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDays}:DATE:`; break;
case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHours}:DATE:`; break;
case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinutes}:DATE:`; break;
case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSeconds}:DATE:`; break;
}
}
}
}
}
}
}

View File

@@ -1,104 +0,0 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core';
import { StateService } from '../../services/state.service';
import { dates } from '../../shared/i18n/dates';
@Component({
selector: 'app-time-until',
template: `{{ text }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimeUntilComponent implements OnInit, OnChanges, OnDestroy {
interval: number;
text: string;
intervals = {};
@Input() time: number;
@Input() fastRender = false;
@Input() fixedRender = false;
@Input() forceFloorOnTimeIntervals: string[];
constructor(
private ref: ChangeDetectorRef,
private stateService: StateService,
) {
this.intervals = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1
};
}
ngOnInit() {
if(this.fixedRender){
this.text = this.calculate();
return;
}
if (!this.stateService.isBrowser) {
this.text = this.calculate();
this.ref.markForCheck();
return;
}
this.interval = window.setInterval(() => {
this.text = this.calculate();
this.ref.markForCheck();
}, 1000 * (this.fastRender ? 1 : 60));
}
ngOnChanges() {
this.text = this.calculate();
this.ref.markForCheck();
}
ngOnDestroy() {
clearInterval(this.interval);
}
calculate() {
const seconds = (+new Date(this.time) - +new Date()) / 1000;
if (seconds < 60) {
const dateStrings = dates(1);
return $localize`:@@time-until:In ~${dateStrings.i18nMinute}:DATE:`;
}
let counter: number;
for (const i in this.intervals) {
if (this.intervals.hasOwnProperty(i)) {
if (this.forceFloorOnTimeIntervals && this.forceFloorOnTimeIntervals.indexOf(i) > -1) {
counter = Math.floor(seconds / this.intervals[i]);
} else {
counter = Math.round(seconds / this.intervals[i]);
}
const dateStrings = dates(counter);
if (counter > 0) {
if (counter === 1) {
switch (i) { // singular (In ~1 day)
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYear}:DATE:`; break;
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonth}:DATE:`; break;
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeek}:DATE:`; break;
case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDay}:DATE:`; break;
case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHour}:DATE:`; break;
case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinute}:DATE:`;
case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSecond}:DATE:`;
}
} else {
switch (i) { // plural (In ~2 days)
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYears}:DATE:`; break;
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonths}:DATE:`; break;
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeeks}:DATE:`; break;
case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDays}:DATE:`; break;
case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHours}:DATE:`; break;
case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinutes}:DATE:`; break;
case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSeconds}:DATE:`; break;
}
}
}
}
}
}
}

View File

@@ -0,0 +1,196 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core';
import { StateService } from '../../services/state.service';
import { dates } from '../../shared/i18n/dates';
@Component({
selector: 'app-time',
template: `{{ text }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimeComponent implements OnInit, OnChanges, OnDestroy {
interval: number;
text: string;
intervals = {};
@Input() time: number;
@Input() dateString: number;
@Input() kind: 'plain' | 'since' | 'until' | 'span' = 'plain';
@Input() fastRender = false;
@Input() fixedRender = false;
@Input() relative = false;
@Input() forceFloorOnTimeIntervals: string[];
@Input() fractionDigits: number = 0;
constructor(
private ref: ChangeDetectorRef,
private stateService: StateService,
) {
this.intervals = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1
};
}
ngOnInit() {
if(this.fixedRender){
this.text = this.calculate();
return;
}
if (!this.stateService.isBrowser) {
this.text = this.calculate();
this.ref.markForCheck();
return;
}
this.interval = window.setInterval(() => {
this.text = this.calculate();
this.ref.markForCheck();
}, 1000 * (this.fastRender ? 1 : 60));
}
ngOnChanges() {
this.text = this.calculate();
this.ref.markForCheck();
}
ngOnDestroy() {
clearInterval(this.interval);
}
calculate() {
let seconds: number;
switch (this.kind) {
case 'since':
seconds = Math.floor((+new Date() - +new Date(this.dateString || this.time * 1000)) / 1000);
break;
case 'until':
seconds = (+new Date(this.time) - +new Date()) / 1000;
break;
default:
seconds = Math.floor(this.time);
}
if (seconds < 60) {
if (this.relative || this.kind === 'since') {
return $localize`:@@date-base.just-now:Just now`;
} else if (this.kind === 'until') {
seconds = 60;
}
}
let counter: number;
for (const i in this.intervals) {
if (this.kind !== 'until' || this.forceFloorOnTimeIntervals && this.forceFloorOnTimeIntervals.indexOf(i) > -1) {
counter = Math.floor(seconds / this.intervals[i]);
} else {
counter = Math.round(seconds / this.intervals[i]);
}
let rounded = counter;
if (this.fractionDigits) {
const roundFactor = Math.pow(10,this.fractionDigits);
rounded = Math.round((seconds / this.intervals[i]) * roundFactor) / roundFactor;
}
const dateStrings = dates(rounded);
if (counter > 0) {
switch (this.kind) {
case 'since':
if (counter === 1) {
switch (i) { // singular (1 day)
case 'year': return $localize`:@@time-since:${dateStrings.i18nYear}:DATE: ago`; break;
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonth}:DATE: ago`; break;
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeek}:DATE: ago`; break;
case 'day': return $localize`:@@time-since:${dateStrings.i18nDay}:DATE: ago`; break;
case 'hour': return $localize`:@@time-since:${dateStrings.i18nHour}:DATE: ago`; break;
case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinute}:DATE: ago`; break;
case 'second': return $localize`:@@time-since:${dateStrings.i18nSecond}:DATE: ago`; break;
}
} else {
switch (i) { // plural (2 days)
case 'year': return $localize`:@@time-since:${dateStrings.i18nYears}:DATE: ago`; break;
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonths}:DATE: ago`; break;
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeeks}:DATE: ago`; break;
case 'day': return $localize`:@@time-since:${dateStrings.i18nDays}:DATE: ago`; break;
case 'hour': return $localize`:@@time-since:${dateStrings.i18nHours}:DATE: ago`; break;
case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinutes}:DATE: ago`; break;
case 'second': return $localize`:@@time-since:${dateStrings.i18nSeconds}:DATE: ago`; break;
}
}
break;
case 'until':
if (counter === 1) {
switch (i) { // singular (In ~1 day)
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYear}:DATE:`; break;
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonth}:DATE:`; break;
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeek}:DATE:`; break;
case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDay}:DATE:`; break;
case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHour}:DATE:`; break;
case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinute}:DATE:`;
case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSecond}:DATE:`;
}
} else {
switch (i) { // plural (In ~2 days)
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYears}:DATE:`; break;
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonths}:DATE:`; break;
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeeks}:DATE:`; break;
case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDays}:DATE:`; break;
case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHours}:DATE:`; break;
case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinutes}:DATE:`; break;
case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSeconds}:DATE:`; break;
}
}
break;
case 'span':
if (counter === 1) {
switch (i) { // singular (1 day)
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYear}:DATE:`; break;
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonth}:DATE:`; break;
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeek}:DATE:`; break;
case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDay}:DATE:`; break;
case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHour}:DATE:`; break;
case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinute}:DATE:`; break;
case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSecond}:DATE:`; break;
}
} else {
switch (i) { // plural (2 days)
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYears}:DATE:`; break;
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonths}:DATE:`; break;
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeeks}:DATE:`; break;
case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDays}:DATE:`; break;
case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHours}:DATE:`; break;
case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinutes}:DATE:`; break;
case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSeconds}:DATE:`; break;
}
}
break;
default:
if (counter === 1) {
switch (i) { // singular (1 day)
case 'year': return dateStrings.i18nYear; break;
case 'month': return dateStrings.i18nMonth; break;
case 'week': return dateStrings.i18nWeek; break;
case 'day': return dateStrings.i18nDay; break;
case 'hour': return dateStrings.i18nHour; break;
case 'minute': return dateStrings.i18nMinute; break;
case 'second': return dateStrings.i18nSecond; break;
}
} else {
switch (i) { // plural (2 days)
case 'year': return dateStrings.i18nYears; break;
case 'month': return dateStrings.i18nMonths; break;
case 'week': return dateStrings.i18nWeeks; break;
case 'day': return dateStrings.i18nDays; break;
case 'hour': return dateStrings.i18nHours; break;
case 'minute': return dateStrings.i18nMinutes; break;
case 'second': return dateStrings.i18nSeconds; break;
}
}
}
}
}
}
}

View File

@@ -57,17 +57,17 @@
<td>
&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
<i class="symbol">(<app-time-since [time]="tx.status.block_time" [fastRender]="true"></app-time-since>)</i>
<i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true"></app-time>)</i>
</div>
</td>
</tr>
<ng-template [ngIf]="transactionTime > 0">
<tr>
<td i18n="transaction.confirmed|Transaction Confirmed state">Confirmed</td>
<td><app-time-span [time]="tx.status.block_time - transactionTime" [fastRender]="true"></app-time-span></td>
<td><app-time kind="span" [time]="tx.status.block_time - transactionTime" [fastRender]="true" [relative]="true"></app-time></td>
</tr>
</ng-template>
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet' && featuresEnabled">
<td class="td-width" i18n="transaction.features|Transaction features">Features</td>
<td>
<app-tx-features [tx]="tx"></app-tx-features>
@@ -100,7 +100,7 @@
<ng-template #firstSeenTmpl>
<tr>
<td i18n="transaction.first-seen|Transaction first seen">First seen</td>
<td><i><app-time-since [time]="transactionTime" [fastRender]="true"></app-time-since></i></td>
<td><i><app-time kind="since" [time]="transactionTime" [fastRender]="true"></app-time></i></td>
</tr>
</ng-template>
</ng-template>
@@ -116,10 +116,10 @@
</ng-template>
<ng-template #belowBlockLimit>
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeEstimateDefault">
<app-time-until [time]="(60 * 1000 * txInBlockIndex) + now" [fastRender]="false" [fixedRender]="true"></app-time-until>
<app-time kind="until" [time]="(60 * 1000 * txInBlockIndex) + now" [fastRender]="false" [fixedRender]="true"></app-time>
</ng-template>
<ng-template #timeEstimateDefault>
<app-time-until *ngIf="(timeAvg$ | async) as timeAvg;" [time]="(timeAvg * txInBlockIndex) + now + timeAvg" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time-until>
<app-time kind="until" *ngIf="(timeAvg$ | async) as timeAvg;" [time]="(timeAvg * txInBlockIndex) + now + timeAvg" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time>
</ng-template>
</ng-template>
</ng-template>
@@ -210,6 +210,7 @@
<div class="graph-container" #graphContainer>
<tx-bowtie-graph
[tx]="tx"
[cached]="isCached"
[width]="graphWidth"
[height]="graphHeight"
[lineLimit]="inOutLimit"
@@ -250,7 +251,7 @@
</div>
<app-transactions-list #txList [transactions]="[tx]" [errorUnblinded]="errorUnblinded" [inputIndex]="inputIndex" [outputIndex]="outputIndex" [transactionPage]="true"></app-transactions-list>
<app-transactions-list #txList [transactions]="[tx]" [cached]="isCached" [errorUnblinded]="errorUnblinded" [inputIndex]="inputIndex" [outputIndex]="outputIndex" [transactionPage]="true"></app-transactions-list>
<div class="title text-left">
<h2 i18n="transaction.details">Details</h2>
@@ -467,6 +468,7 @@
<ng-template #feeTable>
<table class="table table-borderless table-striped">
<tbody>
<tr *ngIf="isMobile && (network === 'liquid' || network === 'liquidtestnet' || !featuresEnabled)"></tr>
<tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [blockConversion]="blockConversion" [value]="tx.fee"></app-fiat></span></td>
@@ -487,7 +489,7 @@
<div class="effective-fee-container">
{{ tx.effectiveFeePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
<ng-template [ngIf]="tx.status.confirmed">
<app-tx-fee-rating class="d-none d-lg-inline ml-2" *ngIf="tx.fee" [tx]="tx"></app-tx-fee-rating>
<app-tx-fee-rating class="ml-2 mr-2" *ngIf="tx.fee || tx.effectiveFeePerVsize" [tx]="tx"></app-tx-fee-rating>
</ng-template>
</div>
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>

View File

@@ -23,6 +23,7 @@ import { BlockExtended, CpfpInfo } from '../../interfaces/node-api.interface';
import { LiquidUnblinding } from './liquid-ublinding';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { Price, PriceService } from '../../services/price.service';
import { isFeatureActive } from '../../bitcoin.utils';
@Component({
selector: 'app-transaction',
@@ -57,6 +58,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
fetchCpfp$ = new Subject<string>();
fetchRbfHistory$ = new Subject<string>();
fetchCachedTx$ = new Subject<string>();
isCached: boolean = false;
now = new Date().getTime();
timeAvg$: Observable<number>;
liquidUnblinding = new LiquidUnblinding();
@@ -73,6 +75,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
flowEnabled: boolean;
blockConversion: Price;
tooltipPosition: { x: number, y: number };
isMobile: boolean;
featuresEnabled: boolean;
segwitEnabled: boolean;
rbfEnabled: boolean;
taprootEnabled: boolean;
@ViewChild('graphContainer')
graphContainer: ElementRef;
@@ -196,6 +204,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
this.tx = tx;
this.setFeatures();
this.isCached = true;
if (tx.fee === undefined) {
this.tx.fee = 0;
}
@@ -289,6 +299,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
this.tx = tx;
this.setFeatures();
this.isCached = false;
if (tx.fee === undefined) {
this.tx.fee = 0;
}
@@ -344,7 +356,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.blocksSubscription = this.stateService.blocks$.subscribe(([block, txConfirmed]) => {
this.latestBlock = block;
if (txConfirmed && this.tx) {
if (txConfirmed && this.tx && !this.tx.status.confirmed) {
this.tx.status = {
confirmed: true,
block_height: block.height,
@@ -362,7 +374,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.waitingForTransaction = false;
}
this.rbfTransaction = rbfTransaction;
this.cacheService.setTxCache([this.rbfTransaction]);
this.replaced = true;
if (rbfTransaction && !this.tx) {
this.fetchCachedTx$.next(this.txId);
@@ -426,9 +437,23 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
setFeatures(): void {
if (this.tx) {
this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit');
this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot');
this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf');
} else {
this.segwitEnabled = false;
this.taprootEnabled = false;
this.rbfEnabled = false;
}
this.featuresEnabled = this.segwitEnabled || this.taprootEnabled || this.rbfEnabled;
}
resetTransaction() {
this.error = undefined;
this.tx = null;
this.setFeatures();
this.waitingForTransaction = false;
this.isLoadingTx = true;
this.rbfTransaction = undefined;
@@ -493,8 +518,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
@HostListener('window:resize', ['$event'])
setGraphSize(): void {
this.isMobile = window.innerWidth < 850;
if (this.graphContainer) {
this.graphWidth = this.graphContainer.nativeElement.clientWidth;
setTimeout(() => {
this.graphWidth = this.graphContainer.nativeElement.clientWidth;
}, 1);
}
}

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