Compare commits

...

155 Commits

Author SHA1 Message Date
wiz
b3e47e1438 Exempt localhost and local networks from rate limits in nginx.conf
A user recently reported they were rate limiting from querying their own
node - this PR exempts all LAN addresses from rate limits

Needs testing on Umbrel / RaspiBlitz / MyNode before merging
2021-10-15 12:36:25 +09:00
softsimon
6f9762d50b Merge pull request #868 from MiguelMedeiros/fix-tooltip-dashboard
UI/UX: Fix mempool chart tooltip at dashboard component.
2021-10-08 22:40:29 +04:00
Miguel Medeiros
9bf475bc97 Fix filter color. 2021-10-08 15:30:26 -03:00
Miguel Medeiros
e59a318cad Fix mempool chart tooltip at dashboard component. 2021-10-08 15:11:59 -03:00
softsimon
57b64f64ad Merge pull request #867 from MiguelMedeiros/fix-tooltip-charts
UI/UX: Fix fee rate tiers on graphs.
2021-10-08 21:54:36 +04:00
Miguel Medeiros
af3af5f099 Fix tooltip ranges. 2021-10-08 12:42:43 -03:00
softsimon
fec603d5c5 Merge pull request #866 from mempool/wiz/fix-ios-top-notch-color
Set the iOS status bar when viewing as progressive web app
2021-10-08 15:36:34 +04:00
softsimon
ed2ebb1c70 Merge pull request #862 from MiguelMedeiros/ui-tooltip-size
UI/UX: Make tooltip looks bigger on mempool fee chart.
2021-10-08 13:59:45 +04:00
wiz
14d2f8dd97 Set the iOS status bar when viewing as progressive web app 2021-10-08 18:23:21 +09:00
wiz
bf563cc195 Merge pull request #847 from MiguelMedeiros/add-filtering-mempool-charts
Add mempool chart filtering.
2021-10-08 09:54:28 +09:00
Miguel Medeiros
f66e0a2c12 Make tooltip style looks bigger 2021-10-07 16:19:57 -03:00
Miguel Medeiros
a43cd48795 Remove unecessary code to controle legends.
Fix order of active and inactive fee ranges.
2021-10-07 16:03:21 -03:00
Miguel Medeiros
44339daedf Add toggle button to dropdown menu.
Revert left margin from tv page.
Change text dropdown filter to icon.
Change dropdown inactive item color.
Revert 500 limit rate.
2021-10-07 14:20:06 -03:00
Miguel Medeiros
14b7b6427a Change dropdown button text. 2021-10-07 14:20:06 -03:00
Miguel Medeiros
a2e866d15a Change filters to dropdown selection menu. 2021-10-07 14:20:05 -03:00
Miguel Medeiros
c2f288a861 Add mempool chart filtering. 2021-10-07 14:20:05 -03:00
wiz
e1c943d0a7 Merge pull request #856 from mempool/simon/mempool-blocks-amount-display-fix
Display correct amount of mempool blocks on mobile
2021-10-07 08:03:42 +09:00
softsimon
fa2d2e60b5 Merge pull request #857 from mempool/wiz/fix-robots-txt
Remove sitemap.xml from robots.txt
2021-10-06 13:21:40 +04:00
wiz
c919980652 Remove sitemap.xml from robots.txt 2021-10-06 17:36:01 +09:00
softsimon
b48389ae7d Display correct amount of mempool blocks on mobile
fixes #854
2021-10-05 15:40:28 +04:00
wiz
2bac7f9987 Merge pull request #855 from mempool/simon/liquid-dashboard-update
Display mempool graph on the Liquid dashboard
2021-10-05 20:16:32 +09:00
softsimon
acf6fd9db5 Display mempool graph on the Liquid dashboard 2021-10-05 15:08:41 +04:00
wiz
74a9b65e81 Merge pull request #853 from mempool/simon/mempool-blocks-config-fix
Use MEMPOOL_BLOCKS_AMOUNT config in the frontend
2021-10-05 10:06:41 +09:00
softsimon
822c840e54 Use MEMPOOL_BLOCKS_AMOUNT config in the frontend
fixes #852
2021-10-05 04:37:24 +04:00
wiz
6e93ef68fe Merge pull request #848 from mempool/simon/update-preview-image
Update README preview image
2021-10-02 05:37:18 +09:00
wiz
3006deae6e Merge pull request #845 from MiguelMedeiros/add-dashed-line
Add mark line to mempool chart.
2021-10-02 05:36:09 +09:00
softsimon
740f5c2003 Update README.md 2021-10-01 21:45:20 +04:00
Miguel Medeiros
5c9d44e9eb Fix dotted line style at tx chart.
Remove dotted line from inverted mempool chart.
2021-10-01 00:35:08 -03:00
wiz
88527b41e7 Merge pull request #846 from MiguelMedeiros/fix-optimize-series-data
Remove vbytesPipe from series data.
2021-10-01 04:44:08 +09:00
Miguel Medeiros
83cce0c3a7 Remove vbytesPipe from series data. 2021-09-29 21:47:39 -03:00
Miguel Medeiros
e144d0c8e5 Add mark line to mempool chart. 2021-09-29 21:44:13 -03:00
wiz
d72dbc1415 Merge pull request #840 from MiguelMedeiros/fix-confirmation-button-position
Fix confirmations button positioning.
2021-09-30 05:46:08 +09:00
Miguel Medeiros
b857a7c37f Fix rtl transaction title. 2021-09-29 17:13:21 -03:00
Miguel Medeiros
c72c287b27 Fix confirmations button positioning. 2021-09-29 16:16:39 -03:00
wiz
18e0a17d26 Merge pull request #843 from MiguelMedeiros/fix-sum-bar-values
Fix total percentage bar value.
2021-09-30 03:59:44 +09:00
Miguel Medeiros
87eeef5d41 Fix total percentage bar value. 2021-09-29 15:12:54 -03:00
softsimon
76a2fdeea7 Merge pull request #839 from MiguelMedeiros/fix-shadowed-variable
Fix lint 'no-shadowed-variable'.
2021-09-29 17:20:39 +04:00
softsimon
792eb3727c Merge pull request #838 from MiguelMedeiros/fix-localized-numbers-mempool-charts
Fix parse numbers localized.
2021-09-29 17:16:48 +04:00
softsimon
2e0845847d Merge pull request #837 from MiguelMedeiros/fix-circle-dots-mempool-charts
Remove circle symbols when hovering the series.
2021-09-29 17:15:18 +04:00
Miguel Medeiros
8f774e55a8 Fix lint 'no-shadowed-variable'. 2021-09-28 21:04:48 -03:00
Miguel Medeiros
28418640bb Fix parse numbers localized. 2021-09-28 21:00:29 -03:00
Miguel Medeiros
8aae5c1c9c Remove circle symbols when hovering the series.
Fix selected index when hovering the series.
2021-09-28 20:13:08 -03:00
wiz
92048964d1 Merge pull request #831 from MiguelMedeiros/fix-sum-column-ui-mempool-chart
Change total sum column to fixed color.
2021-09-29 02:05:15 +09:00
Miguel Medeiros
b2140c2abe Change total sum column to fixed color. 2021-09-28 10:16:27 -03:00
wiz
d0a8509194 Merge pull request #826 from mempool/simon/bisq-transaciton-filter-fix
Fix for stuck Bisq transaction page when filtering
2021-09-27 19:20:28 +09:00
wiz
aa0c3e6fed Merge pull request #825 from MiguelMedeiros/fix-focus-effect-mempool-chart
Fix the focus effect on the mempool graph.
2021-09-27 19:12:49 +09:00
softsimon
f0462114f3 Fix for stuck Bisq transaction page when filtering
fixes #540
2021-09-27 02:37:57 +04:00
Miguel Medeiros
9e0f9840aa Fix the focus effect on the mempool graph. 2021-09-26 16:59:00 -03:00
wiz
d763c30f6a Merge pull request #824 from mempool/simon/lbtc-widget-minor-ux
L-BTC graph: Minor styling update
2021-09-27 04:07:23 +09:00
wiz
92b69657da Merge pull request #814 from mempool/simon/block-transactions-sorting-fix
Sort block transactions first by height and then time
2021-09-27 04:04:46 +09:00
wiz
d9ec0c1a36 Merge pull request #823 from MiguelMedeiros/fix-tooltip-inverted-chart
Fix tooltip mempool chart hover selection.
2021-09-27 04:03:34 +09:00
wiz
4bf9f8b062 Merge pull request #821 from mempool/simon/post-tx-api
Adding POST /tx API to bitcoind mode
2021-09-27 04:03:25 +09:00
softsimon
eefd8104bb L-BTC graph: Minor styling update 2021-09-26 22:57:37 +04:00
wiz
16e807c4b0 Fix API docs curl example for POST /api/tx 2021-09-27 03:49:20 +09:00
Miguel Medeiros
461296e002 Remove uncessary variable check. 2021-09-26 15:47:08 -03:00
softsimon
86c877c8e9 Adding POST /tx API to bitcoind mode
fixes #777
2021-09-26 22:46:23 +04:00
Miguel Medeiros
80fcceef73 Fix size column order when invert mempool chart. 2021-09-26 15:39:37 -03:00
Miguel Medeiros
b0e54818ae Fix mempool chart tooltip hover selection. 2021-09-26 15:24:29 -03:00
Miguel Medeiros
acbd7f0bde Fix inverted tooltip when invert chart. 2021-09-26 15:17:39 -03:00
wiz
9a6efceb34 Merge pull request #820 from MiguelMedeiros/add-inverted-button
UI/UX: Add inverted feature to mempool fee chart.
2021-09-27 01:36:06 +09:00
Miguel Medeiros
5ce7b55441 Add inverted chart feature. 2021-09-26 11:41:55 -03:00
wiz
a3edaf17cc Merge pull request #819 from mempool/simon/lpeg-liquid-test
Check for data to possibly fix Liquid test
2021-09-26 18:51:25 +09:00
softsimon
5695019216 Check for data to possibly fix Liquid test 2021-09-26 13:41:33 +04:00
wiz
7ab1ce8fc4 Merge pull request #813 from mempool/simon/significant-digits-fix
Fix for fee rounding not using locale
2021-09-26 05:25:21 +09:00
wiz
1f8ec2bd8e Merge pull request #812 from mempool/simon/liquid-block-hash-search-fix
Handle search for Liquid block hashes in search bar.
2021-09-26 05:15:06 +09:00
wiz
78b488466e Merge pull request #810 from mempool/simon/liquid-lbtc-dashboard-widget
L-BTC in circulation dashboard widgete
2021-09-26 05:10:47 +09:00
softsimon
66630743f6 L-BTC in circulation dashboard widget
refs #718
2021-09-25 23:45:10 +04:00
softsimon
ffa18bbe71 Sort block transactions first by height and then time
fixes #770
2021-09-25 16:28:11 +04:00
softsimon
8cb1c5c88c Fix for fee rounding not using locale
fixes #796
2021-09-25 15:50:31 +04:00
softsimon
bb07031362 Handle search for Liquid block hashes in search bar.
fixes #797
2021-09-25 14:37:54 +04:00
softsimon
31a0d44543 Merge pull request #809 from mempool/wiz/update-production-backend-configs
Update production backend configuration files
2021-09-24 22:00:12 +04:00
wiz
f90e19c767 Update production backend configuration files
* Set syslog priority to DEBUG since we're not Raspberry Pi
* Add 2nd core RPC configuration stubs for mainnet / liquid
2021-09-25 01:19:33 +09:00
wiz
800625d80e Merge pull request #799 from mempool/simon/liquid-lbtc-widget
Liquid L-BTC widgets (Backend)
2021-09-24 23:54:33 +09:00
wiz
552540f510 Merge pull request #806 from mempool/simon/angular-12-upgrade
Upgrading to Angular 12 and NgBootstrap 10
2021-09-23 05:10:52 +09:00
softsimon
bbee2dcb5b Upgrading to Angular 12 and NgBootstrap 10 2021-09-22 23:57:05 +04:00
softsimon
e4e338b05a Merge pull request #805 from mempool/wiz/update-unchained-url
Update the URL for Unchained Capital to unchained.com
2021-09-22 13:19:19 +04:00
wiz
061a55b236 Update the URL for Unchained Capital to unchained.com 2021-09-22 17:24:11 +09:00
softsimon
9f0f9230fb Merge pull request #804 from mempool/wiz/remove-warden-integration
Remove warden from list of Community Integrations
2021-09-22 11:13:07 +04:00
wiz
40956c0a23 Remove warden from list of Community Integrations 2021-09-22 02:19:48 +09:00
wiz
f29e35b325 Merge pull request #795 from MiguelMedeiros/bugfix-echarts-fixes
UI/UX: Fix charts css styling.
2021-09-22 02:07:43 +09:00
softsimon
d88efb8b0d Merge pull request #803 from mempool/wiz/add-zeus-integration
Add Zeus LN as Community Integration
2021-09-21 19:24:23 +04:00
Miguel Medeiros
b9489525c6 Revert charts margins. 2021-09-21 09:16:59 -03:00
Miguel Medeiros
8ddcd298b0 Fix axis labels css.
Change series smooth to false.
 Make charts margin smaller to match box container.
2021-09-21 09:16:59 -03:00
wiz
69df6e4dcb Add Zeus LN as Community Integration 2021-09-21 19:13:43 +09:00
softsimon
f3c8e2134b Handle errors gracefully. 2021-09-20 01:02:07 +04:00
wiz
0e25c52e67 Merge pull request #801 from mempool/simon/transifex-update 2021-09-20 02:02:12 +09:00
softsimon
60f41d3181 Transifex language fixes to Romanian, Makedonian and Hungarian. 2021-09-19 20:49:27 +04:00
wiz
50c5244abf Merge pull request #800 from mempool/simon/api-docs-hostname-fix
Always use local hostname for API examples.
2021-09-19 18:36:10 +09:00
softsimon
605c1a980c Always use local hostname for API examples. 2021-09-19 13:17:11 +04:00
softsimon
0d67bc36ee Refactoring the MINFEE node configuration into new configs. 2021-09-19 02:40:16 +04:00
softsimon
aa39bbd091 Elements blockchain parser. Save all peg in/out i the database. 2021-09-19 02:26:44 +04:00
softsimon
641d2ad028 Refactoring Bitcoin RPC client implementation 2021-09-18 13:18:47 +04:00
wiz
d602b20f56 Merge pull request #779 from mempool/simon/core-22-compatibility 2021-09-18 17:51:41 +09:00
softsimon
138f6e4e39 Update backend/src/api/bitcoin/bitcoin-api.interface.ts
Co-authored-by: Miguel Medeiros <miguel@miguelmedeiros.com.br>
2021-09-17 19:22:15 +04:00
softsimon
3e788ecbf9 Handle changes to address RPC api in bitcoin core 22
fixes #778
2021-09-17 19:22:15 +04:00
wiz
2236c6d9a6 Merge pull request #794 from mempool/simon/transifex-pull-20200917 2021-09-17 21:42:38 +09:00
softsimon
2a0a1b0213 Pulled from transifex 2021-09-17 16:27:24 +04:00
wiz
e6e49fd5d6 Merge pull request #793 from mempool/simon/credit-romanian-macedonian-translators 2021-09-17 20:49:14 +09:00
softsimon
f6d5f44469 Credit Romanian and Macedonian translators. 2021-09-17 14:19:53 +04:00
softsimon
51e09ff64f Merge pull request #775 from mempool/wiz/fix-canonical-name-for-three-sites
Set canonical URLs for new 3 site structure
2021-09-17 14:14:59 +04:00
softsimon
07ba2f6ecc Merge pull request #792 from mempool/i18n/add-macedonian
Add new locale: Macedonian (mk)
2021-09-17 13:54:03 +04:00
softsimon
bc42552bec Merge pull request #791 from mempool/i18n/add-romanian
Add new locale: Romanian (ro)
2021-09-17 13:53:35 +04:00
wiz
c6b44a3be9 Merge pull request #782 from TechMiX/rtlFixes
Fix RTL issues
2021-09-17 16:23:07 +09:00
wiz
e1f4de0de3 Set canonical URLs for new 3 site structure 2021-09-17 15:13:52 +09:00
wiz
d22dc0888a Add new locale: Macedonian (mk) 2021-09-17 15:03:52 +09:00
wiz
75fb27c690 Add new locale: Romanian (ro) 2021-09-17 15:02:15 +09:00
softsimon
401506a103 Merge pull request #789 from MiguelMedeiros/bugfix-echarts-mobile-mouseover
Disable mouseover legend for mobile users.
fixes #781
2021-09-17 03:24:10 +04:00
Miguel Medeiros
ab27ea28f0 Disable mouseover legend for mobile users. 2021-09-16 17:30:13 -03:00
softsimon
6e579ce0b6 Merge pull request #787 from mempool/wiz/disable-matomo-cookies
Disable matomo cookies - fixes #784
2021-09-16 17:45:37 +04:00
wiz
e7030cca32 Disable matomo cookies - fixes #784 2021-09-16 22:37:09 +09:00
TechMiX
2c496e9a50 add rtl hinting for new graph + move code rtl hints to style file 2021-09-15 12:06:55 +02:00
TechMiX
014d6dee66 fix various rtl issues 2021-09-15 11:02:04 +02:00
wiz
47ae306a75 Merge pull request #738 from MiguelMedeiros/feature-echarts
Feature: New charts library.
2021-09-15 11:08:11 +09:00
Miguel Medeiros
8b8b06e6ab Remove fee tier legends.
Fix tv page css.
2021-09-14 22:35:51 -03:00
Miguel Medeiros
fa7a45421e Fix linting and unclosed html tag.
Fix no shadowed variable tslint warning.
2021-09-14 22:35:51 -03:00
Miguel Medeiros
d376ba1c61 Move total MvB to the top.
Fix yAxis value.
Fix yAxis value.
Make disks smaller and transparent.
Change opacity on mouseover stack bars.
Add 4th column "sum of vsize" to tooltips table.
Add toggle show/hide tier fees.
Make progress active bar inline with text value.
Add a break line to the timestamp text.
Add colored progress bar with number.
2021-09-14 22:35:50 -03:00
Miguel Medeiros
388aa7fbe3 Fix y axis margin left. 2021-09-14 22:35:50 -03:00
Miguel Medeiros
34695146ee Change renderer to svg. 2021-09-14 22:35:50 -03:00
Miguel Medeiros
9020c618f0 Change renderer to svg.
Fix data structure of mempool graph.
Change tooltip total position (top).
Change tooltip visual of partial porcentage.
2021-09-14 22:35:49 -03:00
Miguel Medeiros
584d091d4e Fix indicator border-radius. 2021-09-14 22:35:49 -03:00
Miguel Medeiros
f434e50a2c Invert the tooltip legends order.
Fix default data to title tooltip MM/dd HH:mm.
Add symbol to tx chart tooltip .
Add accumulative total for tooltip information.
Add 3th column to tooltip with a progress bar.
Add and max span zoom span.
Add feeRate limit input to mempool graph component.
Add showZoom option to mempool graph component.
Remove start animation to match the layout for future SSR.
Remove mouse wheel zoom from small template.
Fix small template style.
2021-09-14 22:35:49 -03:00
Miguel Medeiros
1a7decb91d Add default data to title tooltip MM/dd HH:mm.
Add symbol to tx chart tooltip .
2021-09-14 22:35:48 -03:00
Miguel Medeiros
3574f8639e Add total sum to mempool chart.
Add zoom tools.
Add different theme for charts `big` and `small` (default).
Fix date format on mouseover.
Fix animations on graphs page.
Fix overflow tv page.
Remove `crosshair` on mouseover, changed to `line`.
Fix custom tooltip styles.
Remove inverted button (will add in a future PR).
Remove fee range labels (will add in a future PR).
Fix e2e testing.
2021-09-14 22:35:48 -03:00
Miguel Medeiros
9b956ff88d Add new component incoming-transactions-graph;
Refactor component mempool-graph;
Refactor component fee-distribution-graph;
Add incoming-transactions-graph to dashboard;
Add incoming-transactions-graph to statistics;
Add incoming-transactions-graph to television;
Add mempool-graph to dashboard;
Add mempool-graph to statistics;
Add mempool-graph to television;
Remove chartist.component;
2021-09-14 22:35:47 -03:00
Miguel Medeiros
1a98a14541 Add echart module. 2021-09-14 22:35:47 -03:00
wiz
0088e58c14 Merge pull request #776 from mempool/wiz/add-newsyslog-for-nginx 2021-09-11 19:20:30 +09:00
wiz
3fad765269 Add newsyslog.conf for nginx log rotation - delete logs after 90 days 2021-09-11 08:40:58 +09:00
softsimon
2e122a9be1 Merge pull request #771 from mempool/release/v2.2.2
Bump version number to v2.3.0-dev
2021-09-09 11:41:11 +04:00
wiz
2978d16148 Merge pull request #772 from mempool/wiz/fix-api-docs-for-raspberry-pi-users
Fix API docs examples for Raspberry Pi users using romanz/electrs
2021-09-09 14:44:10 +09:00
wiz
fc58bcb3bc Fix API docs examples for Raspberry Pi users using romanz/electrs 2021-09-09 11:11:01 +09:00
wiz
1696623e2f Bump version to v2.3.0-dev after shipping v2.2.2 2021-09-09 07:27:01 +09:00
wiz
251a1af442 Bump version number for v2.2.2 release 2021-09-09 07:23:36 +09:00
wiz
9bdf42530a Merge pull request #769 from mempool/wiz/fix-api-docs-fees-path
Fix api-docs incorrect API path for fees related methods
2021-09-08 07:29:36 +09:00
wiz
8525fbb177 Fix api-docs incorrect API path for fees related methods 2021-09-08 07:13:23 +09:00
wiz
63a3568481 Merge pull request #768 from mempool/simon/liquid-fetch-unconfidential
Liquid: Display unconfidential address and fix tracking
2021-09-08 06:37:24 +09:00
wiz
e4941740de Merge pull request #765 from mempool/simon/address-regex-fix
Updated address regex to handle all types of addresses.
2021-09-08 06:36:42 +09:00
softsimon
25bd33f7da validate-address API should be there both in esplora and bitcoind mode. 2021-09-07 13:13:29 +04:00
softsimon
2d007b9100 Liquid: Display unconfidential address and fix tracking
fixes #761
2021-09-06 10:20:31 +04:00
softsimon
b71330c606 Lowercase Segwit uppercase addresses for tracking matching. 2021-09-05 00:30:24 +04:00
softsimon
844b640c8c Merge pull request #760 from mempool/wiz/rename-keybase-channels
Update syslog.conf and upgrade/restart scripts for new keybase channels
2021-09-04 23:31:28 +04:00
softsimon
1277e58e68 Updated address regex to handle all types of addresses.
fixes #301
fixes #750
2021-09-04 23:21:15 +04:00
wiz
fde6fe324a Merge pull request #764 from mempool/simon/electrum-error-msg-fix
Handle string error messages.
2021-09-04 09:17:46 +09:00
wiz
4ed114a4d5 Merge pull request #762 from mempool/simon/npm-audit-fix-2021-09-03
Npm audit fix.
2021-09-04 09:16:56 +09:00
wiz
8c2dfea6a6 Merge pull request #716 from MiguelMedeiros/documentation-api
Improvements to API documentation and examples
2021-09-04 09:10:59 +09:00
Miguel Medeiros
a0624df06b Change websocket examples. 2021-09-03 20:13:31 -03:00
Miguel Medeiros
1eedcf900b Fix transaction post curl example. 2021-09-03 19:43:28 -03:00
softsimon
0b077d6fda Handle string error messages.
fixes #763
2021-09-04 01:32:36 +04:00
Miguel Medeiros
80047313e7 Remove unused variables. 2021-09-03 16:50:45 -03:00
Miguel Medeiros
71229b94c8 Set default active tab to liquid network. 2021-09-03 16:48:37 -03:00
Miguel Medeiros
c256daf8c8 Fix document location protocol for curl url. 2021-09-03 16:22:39 -03:00
Miguel Medeiros
ba0fb996d2 Fix curl placeholder url depending on base_module.
Fix currencies wrapper url variable name.
2021-09-03 16:02:05 -03:00
Miguel Medeiros
5977e96034 Fix typo variable name. 2021-09-03 14:35:06 -03:00
Miguel Medeiros
a151c5cddd Add template to documentation.
Add support for BASE_MODULE=[mempool, bisq, liquid].
 Add print results do CommonJS examples.
 Add support for custom domains.
 Remove basecurrency from /volume endpoint.
2021-09-03 07:04:19 -03:00
softsimon
0323fd966d Npm audit fix. 2021-09-03 00:44:23 +04:00
wiz
beb834bc30 Update syslog.conf and upgrade/restart scripts for new keybase channels 2021-09-02 19:30:54 +09:00
108 changed files with 23981 additions and 12436 deletions

View File

@@ -2,7 +2,7 @@
Mempool is the fully featured visualizer, explorer, and API service running on [mempool.space](https://mempool.space/), an open source project developed and operated for the benefit of the Bitcoin community, with a focus on the emerging transaction fee market to help our transition into a multi-layer ecosystem.
![mempool](https://mempool.space/resources/screenshots/v2.2.0-dashboard.png)
![mempool](https://mempool.space/resources/screenshots/v2.2.1-dashboard.png)
## Installation Methods

View File

@@ -12,7 +12,8 @@
"BLOCK_WEIGHT_UNITS": 4000000,
"INITIAL_BLOCKS_AMOUNT": 8,
"MEMPOOL_BLOCKS_AMOUNT": 8,
"PRICE_FEED_UPDATE_INTERVAL": 3600
"PRICE_FEED_UPDATE_INTERVAL": 3600,
"USE_SECOND_NODE_FOR_MINFEE": false
},
"CORE_RPC": {
"HOST": "127.0.0.1",
@@ -28,8 +29,7 @@
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:3000"
},
"CORE_RPC_MINFEE": {
"ENABLED": false,
"SECOND_CORE_RPC": {
"HOST": "127.0.0.1",
"PORT": 8332,
"USERNAME": "mempool",

View File

@@ -1,12 +1,12 @@
{
"name": "mempool-backend",
"version": "2.2.2-dev",
"version": "2.3.0-dev",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "mempool-backend",
"version": "2.2.2-dev",
"version": "2.3.0-dev",
"license": "GNU Affero General Public License v3.0",
"dependencies": {
"@mempool/bitcoin": "^3.0.3",

View File

@@ -1,6 +1,6 @@
{
"name": "mempool-backend",
"version": "2.2.2-dev",
"version": "2.3.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",

View File

@@ -71,7 +71,7 @@ interface BisqScriptPubKey {
addresses: string[];
asm: string;
hex: string;
reqSigs: number;
reqSigs?: number;
type: string;
}

View File

@@ -11,4 +11,12 @@ export interface AbstractBitcoinApi {
$getAddress(address: string): Promise<IEsploraApi.Address>;
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
$getAddressPrefix(prefix: string): string[];
$sendRawTransaction(rawTransaction: string): Promise<string>;
}
export interface BitcoinRpcCredentials {
host: string;
port: number;
user: string;
pass: string;
timeout: number;
}

View File

@@ -3,16 +3,17 @@ import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import EsploraApi from './esplora-api';
import BitcoinApi from './bitcoin-api';
import ElectrumApi from './electrum-api';
import bitcoinClient from './bitcoin-client';
function bitcoinApiFactory(): AbstractBitcoinApi {
switch (config.MEMPOOL.BACKEND) {
case 'esplora':
return new EsploraApi();
case 'electrum':
return new ElectrumApi();
return new ElectrumApi(bitcoinClient);
case 'none':
default:
return new BitcoinApi();
return new BitcoinApi(bitcoinClient);
}
}

View File

@@ -72,7 +72,7 @@ export namespace IBitcoinApi {
time: number; // (numeric) Same as blocktime
}
interface Vin {
export interface Vin {
txid?: string; // (string) The transaction id
vout?: number; // (string)
scriptSig?: { // (json object) The script
@@ -82,28 +82,36 @@ export namespace IBitcoinApi {
sequence: number; // (numeric) The script sequence number
txinwitness?: string[]; // (string) hex-encoded witness data
coinbase?: string;
is_pegin?: boolean; // (boolean) Elements peg-in
}
interface Vout {
export interface Vout {
value: number; // (numeric) The value in BTC
n: number; // (numeric) index
asset?: string; // (string) Elements asset id
scriptPubKey: { // (json object)
asm: string; // (string) the asm
hex: string; // (string) the hex
reqSigs: number; // (numeric) The required sigs
reqSigs?: number; // (numeric) The required sigs
type: string; // (string) The type, eg 'pubkeyhash'
addresses: string[] // (string) bitcoin address
address?: string; // (string) bitcoin address
addresses?: string[]; // (string) bitcoin addresses
pegout_chain?: string; // (string) Elements peg-out chain
pegout_addresses?: string[]; // (string) Elements peg-out addresses
};
}
export interface AddressInformation {
isvalid: boolean; // (boolean) If the address is valid or not. If not, this is the only property returned.
isvalid_parent?: boolean; // (boolean) Elements only
address: string; // (string) The bitcoin address validated
scriptPubKey: string; // (string) The hex-encoded scriptPubKey generated by the address
isscript: boolean; // (boolean) If the key is a script
iswitness: boolean; // (boolean) If the address is a witness
witness_version?: boolean; // (numeric, optional) The version number of the witness program
witness_program: string; // (string, optional) The hex value of the witness program
confidential_key?: string; // (string) Elements only
unconfidential?: string; // (string) Elements only
}
export interface ChainTips {

View File

@@ -1,5 +1,3 @@
import config from '../../config';
import * as bitcoin from '@mempool/bitcoin';
import * as bitcoinjs from 'bitcoinjs-lib';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { IBitcoinApi } from './bitcoin-api.interface';
@@ -10,16 +8,10 @@ import { TransactionExtended } from '../../mempool.interfaces';
class BitcoinApi implements AbstractBitcoinApi {
private rawMempoolCache: IBitcoinApi.RawMempool | null = null;
private bitcoindClient: any;
protected bitcoindClient: any;
constructor() {
this.bitcoindClient = new bitcoin.Client({
host: config.CORE_RPC.HOST,
port: config.CORE_RPC.PORT,
user: config.CORE_RPC.USERNAME,
pass: config.CORE_RPC.PASSWORD,
timeout: 60000,
});
constructor(bitcoinClient: any) {
this.bitcoindClient = bitcoinClient;
}
$getRawTransaction(txId: string, skipConversion = false, addPrevout = false): Promise<IEsploraApi.Transaction> {
@@ -60,13 +52,12 @@ class BitcoinApi implements AbstractBitcoinApi {
return this.bitcoindClient.getBlock(hash, 0);
}
$getBlockHash(height: number): Promise<string> {
return this.bitcoindClient.getBlockHash(height);
}
$getBlockHeader(hash: string): Promise<string> {
return this.bitcoindClient.getBlockHeader(hash,false);
return this.bitcoindClient.getBlockHeader(hash, false);
}
async $getBlock(hash: string): Promise<IEsploraApi.Block> {
@@ -107,6 +98,10 @@ class BitcoinApi implements AbstractBitcoinApi {
return found;
}
$sendRawTransaction(rawTransaction: string): Promise<string> {
return this.bitcoindClient.sendRawTransaction(rawTransaction);
}
protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean): Promise<IEsploraApi.Transaction> {
let esploraTransaction: IEsploraApi.Transaction = {
txid: transaction.txid,
@@ -124,7 +119,8 @@ class BitcoinApi implements AbstractBitcoinApi {
return {
value: vout.value * 100000000,
scriptpubkey: vout.scriptPubKey.hex,
scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '',
scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.address ? vout.scriptPubKey.address
: vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '',
scriptpubkey_asm: vout.scriptPubKey.asm ? this.convertScriptSigAsm(vout.scriptPubKey.asm) : '',
scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type),
};
@@ -238,10 +234,6 @@ class BitcoinApi implements AbstractBitcoinApi {
});
}
protected $validateAddress(address: string): Promise<IBitcoinApi.AddressInformation> {
return this.bitcoindClient.validateAddress(address);
}
private $getMempoolEntry(txid: string): Promise<IBitcoinApi.MempoolEntry> {
return this.bitcoindClient.getMempoolEntry(txid);
}

View File

@@ -1,49 +0,0 @@
import config from '../../config';
import * as bitcoin from '@mempool/bitcoin';
import { IBitcoinApi } from './bitcoin-api.interface';
class BitcoinBaseApi {
bitcoindClient: any;
bitcoindClientMempoolInfo: any;
constructor() {
this.bitcoindClient = new bitcoin.Client({
host: config.CORE_RPC.HOST,
port: config.CORE_RPC.PORT,
user: config.CORE_RPC.USERNAME,
pass: config.CORE_RPC.PASSWORD,
timeout: 60000,
});
if (config.CORE_RPC_MINFEE.ENABLED) {
this.bitcoindClientMempoolInfo = new bitcoin.Client({
host: config.CORE_RPC_MINFEE.HOST,
port: config.CORE_RPC_MINFEE.PORT,
user: config.CORE_RPC_MINFEE.USERNAME,
pass: config.CORE_RPC_MINFEE.PASSWORD,
timeout: 60000,
});
}
}
$getMempoolInfo(): Promise<IBitcoinApi.MempoolInfo> {
if (config.CORE_RPC_MINFEE.ENABLED) {
return Promise.all([
this.bitcoindClient.getMempoolInfo(),
this.bitcoindClientMempoolInfo.getMempoolInfo()
]).then(([mempoolInfo, secondMempoolInfo]) => {
mempoolInfo.maxmempool = secondMempoolInfo.maxmempool;
mempoolInfo.mempoolminfee = secondMempoolInfo.mempoolminfee;
mempoolInfo.minrelaytxfee = secondMempoolInfo.minrelaytxfee;
return mempoolInfo;
});
}
return this.bitcoindClient.getMempoolInfo();
}
$getBlockchainInfo(): Promise<IBitcoinApi.BlockchainInfo> {
return this.bitcoindClient.getBlockchainInfo();
}
}
export default new BitcoinBaseApi();

View File

@@ -0,0 +1,13 @@
import config from '../../config';
import * as bitcoin from '@mempool/bitcoin';
import { BitcoinRpcCredentials } from './bitcoin-api-abstract-factory';
const nodeRpcCredentials: BitcoinRpcCredentials = {
host: config.CORE_RPC.HOST,
port: config.CORE_RPC.PORT,
user: config.CORE_RPC.USERNAME,
pass: config.CORE_RPC.PASSWORD,
timeout: 60000,
};
export default new bitcoin.Client(nodeRpcCredentials);

View File

@@ -0,0 +1,13 @@
import config from '../../config';
import * as bitcoin from '@mempool/bitcoin';
import { BitcoinRpcCredentials } from './bitcoin-api-abstract-factory';
const nodeRpcCredentials: BitcoinRpcCredentials = {
host: config.SECOND_CORE_RPC.HOST,
port: config.SECOND_CORE_RPC.PORT,
user: config.SECOND_CORE_RPC.USERNAME,
pass: config.SECOND_CORE_RPC.PASSWORD,
timeout: 60000,
};
export default new bitcoin.Client(nodeRpcCredentials);

View File

@@ -1,10 +1,8 @@
import config from '../../config';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { IBitcoinApi } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface';
import { IElectrumApi } from './electrum-api.interface';
import BitcoinApi from './bitcoin-api';
import mempool from '../mempool';
import logger from '../../logger';
import * as ElectrumClient from '@mempool/electrum-client';
import * as sha256 from 'crypto-js/sha256';
@@ -15,8 +13,8 @@ import memoryCache from '../memory-cache';
class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
private electrumClient: any;
constructor() {
super();
constructor(bitcoinClient: any) {
super(bitcoinClient);
const electrumConfig = { client: 'mempool-v2', version: '1.4' };
const electrumPersistencePolicy = { retryPeriod: 10000, maxRetry: 1000, callback: null };
@@ -44,7 +42,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
}
async $getAddress(address: string): Promise<IEsploraApi.Address> {
const addressInfo = await this.$validateAddress(address);
const addressInfo = await this.bitcoindClient.validateAddress(address);
if (!addressInfo || !addressInfo.isvalid) {
return ({
'address': address,
@@ -93,12 +91,12 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
if (e === 'failed to get confirmed status') {
e = 'The number of transactions on this address exceeds the Electrum server limit';
}
throw new Error(e instanceof Error ? e.message : 'Error');
throw new Error(typeof e === 'string' ? e : 'Error');
}
}
async $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
const addressInfo = await this.$validateAddress(address);
const addressInfo = await this.bitcoindClient.validateAddress(address);
if (!addressInfo || !addressInfo.isvalid) {
return [];
}
@@ -131,7 +129,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
if (e === 'failed to get confirmed status') {
e = 'The number of transactions on this address exceeds the Electrum server limit';
}
throw new Error(e instanceof Error ? e.message : 'Error');
throw new Error(typeof e === 'string' ? e : 'Error');
}
}

View File

@@ -56,6 +56,10 @@ class ElectrsApi implements AbstractBitcoinApi {
$getAddressPrefix(prefix: string): string[] {
throw new Error('Method not implemented.');
}
$sendRawTransaction(rawTransaction: string): Promise<string> {
throw new Error('Method not implemented.');
}
}
export default ElectrsApi;

View File

@@ -6,7 +6,7 @@ import { BlockExtended, TransactionExtended } from '../mempool.interfaces';
import { Common } from './common';
import diskCache from './disk-cache';
import transactionUtils from './transaction-utils';
import bitcoinBaseApi from './bitcoin/bitcoin-base.api';
import bitcoinClient from './bitcoin/bitcoin-client';
class Blocks {
private blocks: BlockExtended[] = [];
@@ -45,7 +45,7 @@ class Blocks {
}
if (!this.lastDifficultyAdjustmentTime) {
const blockchainInfo = await bitcoinBaseApi.$getBlockchainInfo();
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
if (blockchainInfo.blocks === blockchainInfo.headers) {
const heightDiff = blockHeightTip % 2016;
const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff);

View File

@@ -1,6 +1,8 @@
import { CpfpInfo, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
import config from '../config';
export class Common {
static nativeAssetId = '6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d';
static median(numbers: number[]) {
let medianNr = 0;
const numsLen = numbers.length;

View File

@@ -0,0 +1,111 @@
import { IBitcoinApi } from '../bitcoin/bitcoin-api.interface';
import bitcoinClient from '../bitcoin/bitcoin-client';
import bitcoinSecondClient from '../bitcoin/bitcoin-second-client';
import { Common } from '../common';
import { DB } from '../../database';
import logger from '../../logger';
class ElementsParser {
private isRunning = false;
constructor() { }
public async $parse() {
if (this.isRunning) {
return;
}
try {
this.isRunning = true;
const result = await bitcoinClient.getChainTips();
const tip = result[0].height;
const latestBlock = await this.$getLatestBlockFromDatabase();
for (let height = latestBlock.block + 1; height <= tip; height++) {
const blockHash: IBitcoinApi.ChainTips = await bitcoinClient.getBlockHash(height);
const block: IBitcoinApi.Block = await bitcoinClient.getBlock(blockHash, 2);
await this.$parseBlock(block);
await this.$saveLatestBlockToDatabase(block.height, block.time, block.hash);
}
this.isRunning = false;
} catch (e) {
this.isRunning = false;
throw new Error(e instanceof Error ? e.message : 'Error');
}
}
public async $getPegDataByMonth(): Promise<any> {
const connection = await DB.pool.getConnection();
const query = `SELECT SUM(amount) AS amount, DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y-%m-01') AS date FROM elements_pegs GROUP BY DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y%m')`;
const [rows] = await connection.query<any>(query);
connection.release();
return rows;
}
protected async $parseBlock(block: IBitcoinApi.Block) {
for (const tx of block.tx) {
await this.$parseInputs(tx, block);
await this.$parseOutputs(tx, block);
}
}
protected async $parseInputs(tx: IBitcoinApi.Transaction, block: IBitcoinApi.Block) {
for (const [index, input] of tx.vin.entries()) {
if (input.is_pegin) {
await this.$parsePegIn(input, index, tx.txid, block);
}
}
}
protected async $parsePegIn(input: IBitcoinApi.Vin, vindex: number, txid: string, block: IBitcoinApi.Block) {
const bitcoinTx: IBitcoinApi.Transaction = await bitcoinSecondClient.getRawTransaction(input.txid, true);
const prevout = bitcoinTx.vout[input.vout || 0];
const outputAddress = prevout.scriptPubKey.address || (prevout.scriptPubKey.addresses && prevout.scriptPubKey.addresses[0]) || '';
await this.$savePegToDatabase(block.height, block.time, prevout.value * 100000000, txid, vindex,
outputAddress, bitcoinTx.txid, prevout.n, 1);
}
protected async $parseOutputs(tx: IBitcoinApi.Transaction, block: IBitcoinApi.Block) {
for (const output of tx.vout) {
if (output.scriptPubKey.pegout_chain) {
await this.$savePegToDatabase(block.height, block.time, 0 - output.value * 100000000, tx.txid, output.n,
(output.scriptPubKey.pegout_addresses && output.scriptPubKey.pegout_addresses[0] || ''), '', 0, 0);
}
if (!output.scriptPubKey.pegout_chain && output.scriptPubKey.type === 'nulldata'
&& output.value && output.value > 0 && output.asset && output.asset === Common.nativeAssetId) {
await this.$savePegToDatabase(block.height, block.time, 0 - output.value * 100000000, tx.txid, output.n,
(output.scriptPubKey.pegout_addresses && output.scriptPubKey.pegout_addresses[0] || ''), '', 0, 1);
}
}
}
protected async $savePegToDatabase(height: number, blockTime: number, amount: number, txid: string,
txindex: number, bitcoinaddress: string, bitcointxid: string, bitcoinindex: number, final_tx: number): Promise<void> {
const connection = await DB.pool.getConnection();
const query = `INSERT INTO elements_pegs(
block, datetime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
const params: (string | number)[] = [
height, blockTime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx
];
await connection.query(query, params);
connection.release();
logger.debug(`Saved L-BTC peg from block height #${height} with TXID ${txid}.`);
}
protected async $getLatestBlockFromDatabase(): Promise<any> {
const connection = await DB.pool.getConnection();
const query = `SELECT block, datetime, block_hash FROM last_elements_block`;
const [rows] = await connection.query<any>(query);
connection.release();
return rows[0];
}
protected async $saveLatestBlockToDatabase(blockHeight: number, datetime: number, blockHash: string) {
const connection = await DB.pool.getConnection();
const query = `UPDATE last_elements_block SET block = ?, datetime = ?, block_hash = ?`;
await connection.query<any>(query, [blockHeight, datetime, blockHash]);
connection.release();
}
}
export default new ElementsParser();

View File

@@ -75,7 +75,8 @@ class MempoolBlocks {
let blockSize = 0;
let transactions: TransactionExtended[] = [];
transactionsSorted.forEach((tx) => {
if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS || mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT) {
if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS
|| mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) {
blockWeight += tx.weight;
blockSize += tx.size;
transactions.push(tx);

View File

@@ -5,8 +5,9 @@ import logger from '../logger';
import { Common } from './common';
import transactionUtils from './transaction-utils';
import { IBitcoinApi } from './bitcoin/bitcoin-api.interface';
import bitcoinBaseApi from './bitcoin/bitcoin-base.api';
import loadingIndicators from './loading-indicators';
import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
class Mempool {
private static WEBSOCKET_REFRESH_RATE_MS = 10000;
@@ -61,7 +62,7 @@ class Mempool {
}
public async $updateMemPoolInfo() {
this.mempoolInfo = await bitcoinBaseApi.$getMempoolInfo();
this.mempoolInfo = await this.$getMempoolInfo();
}
public getMempoolInfo(): IBitcoinApi.MempoolInfo {
@@ -205,6 +206,21 @@ class Mempool {
}
}
}
private $getMempoolInfo() {
if (config.MEMPOOL.USE_SECOND_NODE_FOR_MINFEE) {
return Promise.all([
bitcoinClient.getMempoolInfo(),
bitcoinSecondClient.getMempoolInfo()
]).then(([mempoolInfo, secondMempoolInfo]) => {
mempoolInfo.maxmempool = secondMempoolInfo.maxmempool;
mempoolInfo.mempoolminfee = secondMempoolInfo.mempoolminfee;
mempoolInfo.minrelaytxfee = secondMempoolInfo.minrelaytxfee;
return mempoolInfo;
});
}
return bitcoinClient.getMempoolInfo();
}
}
export default new Mempool();

View File

@@ -14,7 +14,6 @@ import transactionUtils from './transaction-utils';
class WebsocketHandler {
private wss: WebSocket.Server | undefined;
private nativeAssetId = '6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d';
private extraInitProperties = {};
constructor() { }
@@ -80,9 +79,13 @@ class WebsocketHandler {
}
if (parsedMessage && parsedMessage['track-address']) {
if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,87})$/
if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/
.test(parsedMessage['track-address'])) {
client['track-address'] = parsedMessage['track-address'];
let matchedAddress = parsedMessage['track-address'];
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(parsedMessage['track-address'])) {
matchedAddress = matchedAddress.toLowerCase();
}
client['track-address'] = matchedAddress;
} else {
client['track-address'] = null;
}
@@ -304,7 +307,7 @@ class WebsocketHandler {
newTransactions.forEach((tx) => {
if (client['track-asset'] === this.nativeAssetId) {
if (client['track-asset'] === Common.nativeAssetId) {
if (tx.vin.some((vin) => !!vin.is_pegin)) {
foundTransactions.push(tx);
return;
@@ -435,7 +438,7 @@ class WebsocketHandler {
const foundTransactions: TransactionExtended[] = [];
transactions.forEach((tx) => {
if (client['track-asset'] === this.nativeAssetId) {
if (client['track-asset'] === Common.nativeAssetId) {
if (tx.vin && tx.vin.some((vin) => !!vin.is_pegin)) {
foundTransactions.push(tx);
return;

View File

@@ -15,6 +15,7 @@ interface IConfig {
INITIAL_BLOCKS_AMOUNT: number;
MEMPOOL_BLOCKS_AMOUNT: number;
PRICE_FEED_UPDATE_INTERVAL: number;
USE_SECOND_NODE_FOR_MINFEE: boolean;
};
ESPLORA: {
REST_API_URL: string;
@@ -30,8 +31,7 @@ interface IConfig {
USERNAME: string;
PASSWORD: string;
};
CORE_RPC_MINFEE: {
ENABLED: boolean;
SECOND_CORE_RPC: {
HOST: string;
PORT: number;
USERNAME: string;
@@ -77,6 +77,7 @@ const defaults: IConfig = {
'INITIAL_BLOCKS_AMOUNT': 8,
'MEMPOOL_BLOCKS_AMOUNT': 8,
'PRICE_FEED_UPDATE_INTERVAL': 3600,
'USE_SECOND_NODE_FOR_MINFEE': false,
},
'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000',
@@ -92,8 +93,7 @@ const defaults: IConfig = {
'USERNAME': 'mempool',
'PASSWORD': 'mempool'
},
'CORE_RPC_MINFEE': {
'ENABLED': false,
'SECOND_CORE_RPC': {
'HOST': '127.0.0.1',
'PORT': 8332,
'USERNAME': 'mempool',
@@ -129,7 +129,7 @@ class Config implements IConfig {
ESPLORA: IConfig['ESPLORA'];
ELECTRUM: IConfig['ELECTRUM'];
CORE_RPC: IConfig['CORE_RPC'];
CORE_RPC_MINFEE: IConfig['CORE_RPC_MINFEE'];
SECOND_CORE_RPC: IConfig['SECOND_CORE_RPC'];
DATABASE: IConfig['DATABASE'];
SYSLOG: IConfig['SYSLOG'];
STATISTICS: IConfig['STATISTICS'];
@@ -141,7 +141,7 @@ class Config implements IConfig {
this.ESPLORA = configs.ESPLORA;
this.ELECTRUM = configs.ELECTRUM;
this.CORE_RPC = configs.CORE_RPC;
this.CORE_RPC_MINFEE = configs.CORE_RPC_MINFEE;
this.SECOND_CORE_RPC = configs.SECOND_CORE_RPC;
this.DATABASE = configs.DATABASE;
this.SYSLOG = configs.SYSLOG;
this.STATISTICS = configs.STATISTICS;

View File

@@ -20,6 +20,7 @@ import logger from './logger';
import backendInfo from './api/backend-info';
import loadingIndicators from './api/loading-indicators';
import mempool from './api/mempool';
import elementsParser from './api/liquid/elements-parser';
class Server {
private wss: WebSocket.Server | undefined;
@@ -112,7 +113,7 @@ class Server {
await memPool.$updateMemPoolInfo();
} catch (e) {
const msg = `updateMempoolInfo: ${(e instanceof Error ? e.message : e)}`;
if (config.CORE_RPC_MINFEE.ENABLED) {
if (config.MEMPOOL.USE_SECOND_NODE_FOR_MINFEE) {
logger.warn(msg);
} else {
logger.debug(msg);
@@ -141,6 +142,15 @@ class Server {
if (this.wss) {
websocketHandler.setWebsocketServer(this.wss);
}
if (config.MEMPOOL.NETWORK === 'liquid') {
blocks.setNewBlockCallback(async () => {
try {
await elementsParser.$parse();
} catch (e) {
logger.warn('Elements parsing error: ' + (e instanceof Error ? e.message : e));
}
});
}
websocketHandler.setupConnectionHandling();
statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler));
blocks.setNewBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
@@ -158,6 +168,7 @@ class Server {
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', routes.getMempoolBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', routes.getBackendInfo)
.get(config.MEMPOOL.API_URL_PREFIX + 'init-data', routes.getInitData)
.get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', routes.validateAddress)
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
try {
const response = await axios.get('https://mempool.space/api/v1/donations', { responseType: 'stream', timeout: 10000 });
@@ -235,6 +246,7 @@ class Server {
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool/txids', routes.getMempoolTxIds)
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', routes.getRecentMempoolTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', routes.getTransaction)
.post(config.MEMPOOL.API_URL_PREFIX + 'tx', routes.$postTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', routes.getRawTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', routes.getTransactionStatus)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', routes.getTransactionOutspends)
@@ -253,6 +265,12 @@ class Server {
.get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', routes.getAddressPrefix)
;
}
if (config.MEMPOOL.NETWORK === 'liquid') {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', routes.$getElementsPegsByMonth)
;
}
}
}

View File

@@ -17,6 +17,8 @@ import transactionUtils from './api/transaction-utils';
import blocks from './api/blocks';
import loadingIndicators from './api/loading-indicators';
import { Common } from './api/common';
import bitcoinClient from './api/bitcoin/bitcoin-client';
import elementsParser from './api/liquid/elements-parser';
class Routes {
constructor() {}
@@ -687,6 +689,15 @@ class Routes {
}
}
public async validateAddress(req: Request, res: Response) {
try {
const result = await bitcoinClient.validateAddress(req.params.address);
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public getTransactionOutspends(req: Request, res: Response) {
res.status(501).send('Not implemented');
}
@@ -727,7 +738,7 @@ class Routes {
const timeAvg = timeAvgMins * 60;
const remainingTime = remainingBlocks * timeAvg;
const estimatedRetargetDate = remainingTime + now;
const result = {
progressPercent,
difficultyChange,
@@ -737,13 +748,34 @@ class Routes {
previousRetarget,
nextRetargetHeight,
timeAvg,
}
};
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async $getElementsPegsByMonth(req: Request, res: Response) {
try {
const pegs = await elementsParser.$getPegDataByMonth();
res.json(pegs);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async $postTransaction(req: Request, res: Response) {
res.setHeader('content-type', 'text/plain');
try {
const rawtx = Object.keys(req.body)[0];
const txIdResult = await bitcoinApi.$sendRawTransaction(rawtx);
res.send(txIdResult);
} catch (e: any) {
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
}
export default new Routes();

View File

@@ -32,3 +32,5 @@ https://www.transifex.com/mempool/mempool/dashboard/
* Vietnamese @bitcoin_vietnam
* Chinese @wdljt
* Russian @TonyCrusoe @Bitconan
* Romanian @mirceavesa
* Macedonian @SkechBoy

View File

@@ -114,10 +114,18 @@
"translation": "src/locale/messages.hu.xlf",
"baseHref": "/hu/"
},
"mk": {
"translation": "src/locale/messages.mk.xlf",
"baseHref": "/mk/"
},
"zh": {
"translation": "src/locale/messages.zh.xlf",
"baseHref": "/zh/"
},
"ro": {
"translation": "src/locale/messages.ro.xlf",
"baseHref": "/ro/"
},
"ru": {
"translation": "src/locale/messages.ru.xlf",
"baseHref": "/ru/"
@@ -137,7 +145,6 @@
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/resources",
@@ -149,7 +156,13 @@
],
"scripts": [
"generated-config.js"
]
],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true
},
"configurations": {
"production": {
@@ -159,7 +172,14 @@
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false
},
"fonts": true
},
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
@@ -178,7 +198,8 @@
}
]
}
}
},
"defaultConfiguration": ""
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
@@ -262,7 +283,9 @@
"options": {
"outputPath": "dist/mempool/server",
"main": "server.ts",
"tsConfig": "tsconfig.server.json"
"tsConfig": "tsconfig.server.json",
"sourceMap": true,
"optimization": false
},
"configurations": {
"production": {
@@ -277,7 +300,8 @@
"localize": true,
"optimization": true
}
}
},
"defaultConfiguration": ""
},
"serve-ssr": {
"builder": "@nguniversal/builders:ssr-dev-server",

View File

@@ -281,12 +281,12 @@ describe('Mainnet', () => {
});
});
it('loads the tv screen - mobile', () => {
it.only('loads the tv screen - mobile', () => {
cy.viewport('iphone-6');
cy.visit('/tv');
cy.waitForSkeletonGone();
cy.get('.chart-holder');
cy.get('.blockchain-wrapper').should('be.visible');
cy.get('.blockchain-wrapper').should('not.visible');
});
it('loads the api screen', () => {

15754
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "mempool-frontend",
"version": "2.2.2-dev",
"version": "2.3.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",
@@ -22,7 +22,7 @@
"scripts": {
"ng": "./node_modules/@angular/cli/bin/ng",
"tsc": "./node_modules/typescript/bin/tsc",
"i18n-extract-from-source": "./node_modules/@angular/cli/bin/ng extract-i18n --ivy --out-file ./src/locale/messages.xlf",
"i18n-extract-from-source": "./node_modules/@angular/cli/bin/ng extract-i18n --out-file ./src/locale/messages.xlf",
"i18n-pull-from-transifex": "tx pull -a --parallel --minimum-perc 1 --force",
"serve": "npm run generate-config && ng serve -c local",
"serve:stg": "npm run generate-config && ng serve -c staging",
@@ -30,7 +30,7 @@
"start": "npm run generate-config && npm run sync-assets-dev && ng serve -c local",
"start:stg": "npm run generate-config && npm run sync-assets-dev && ng serve -c staging",
"start:local-prod": "npm run generate-config && npm run sync-assets-dev && ng serve -c local-prod",
"build": "npm run generate-config && ng build --prod --localize && npm run sync-assets && npm run build-mempool.js",
"build": "npm run generate-config && ng build --configuration production --localize && npm run sync-assets && npm run build-mempool.js",
"sync-assets": "node sync-assets.js && rsync -av ./dist/mempool/browser/en-US/resources ./dist/mempool/browser/resources",
"sync-assets-dev": "node sync-assets.js dev",
"generate-config": "node generate-config.js",
@@ -53,32 +53,34 @@
"cypress:run:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record"
},
"dependencies": {
"@angular/animations": "~11.2.8",
"@angular/common": "~11.2.8",
"@angular/compiler": "~11.2.8",
"@angular/core": "~11.2.8",
"@angular/forms": "~11.2.8",
"@angular/localize": "^11.2.8",
"@angular/platform-browser": "~11.2.8",
"@angular/platform-browser-dynamic": "~11.2.8",
"@angular/platform-server": "~11.2.8",
"@angular/router": "~11.2.8",
"@angular/animations": "~12.2.6",
"@angular/common": "~12.2.6",
"@angular/compiler": "~12.2.6",
"@angular/core": "~12.2.6",
"@angular/forms": "~12.2.6",
"@angular/localize": "^12.2.6",
"@angular/platform-browser": "~12.2.6",
"@angular/platform-browser-dynamic": "~12.2.6",
"@angular/platform-server": "~12.2.6",
"@angular/router": "~12.2.6",
"@fortawesome/angular-fontawesome": "^0.8.2",
"@fortawesome/fontawesome-common-types": "^0.2.35",
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@mempool/chartist": "^0.11.4",
"@juggle/resize-observer": "^3.3.1",
"@mempool/mempool.js": "^2.2.4",
"@ng-bootstrap/ng-bootstrap": "^7.0.0",
"@ng-bootstrap/ng-bootstrap": "^10.0.0",
"@nguniversal/express-engine": "11.2.1",
"@types/qrcode": "^1.3.4",
"bootstrap": "4.5.0",
"browserify": "^17.0.0",
"clipboard": "^2.0.4",
"domino": "^2.1.6",
"echarts": "^5.1.2",
"express": "^4.17.1",
"lightweight-charts": "^3.3.0",
"ngx-bootrap-multiselect": "^2.0.0",
"ngx-echarts": "^7.0.1",
"ngx-infinite-scroll": "^10.0.1",
"qrcode": "^1.4.4",
"rxjs": "^6.6.7",
@@ -88,10 +90,10 @@
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.1102.7",
"@angular/cli": "~11.2.7",
"@angular/compiler-cli": "~11.2.8",
"@angular/language-service": "~11.2.8",
"@angular-devkit/build-angular": "^12.2.6",
"@angular/cli": "~12.2.6",
"@angular/compiler-cli": "~12.2.6",
"@angular/language-service": "~12.2.6",
"@nguniversal/builders": "^11.2.1",
"@types/express": "^4.17.0",
"@types/jasmine": "~3.6.0",
@@ -101,14 +103,14 @@
"http-proxy-middleware": "^1.0.5",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~6.1.0",
"karma": "~6.3.4",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~4.1.5"
"typescript": "~4.3.5"
},
"optionalDependencies": {
"@cypress/schematic": "^1.3.0",

View File

@@ -1,4 +1,4 @@
import 'zone.js/dist/zone-node';
import 'zone.js/node';
import './generated-config';
import * as domino from 'domino';

View File

@@ -1,4 +1,4 @@
import 'zone.js/dist/zone-node';
import 'zone.js/node';
import './generated-config';
import { ngExpressEngine } from '@nguniversal/express-engine';

View File

@@ -31,6 +31,46 @@ export const mempoolFeeColors = [
'b9254b',
];
export const chartColors = [
"#D81B60",
"#8E24AA",
"#5E35B1",
"#3949AB",
"#1E88E5",
"#039BE5",
"#00ACC1",
"#00897B",
"#43A047",
"#7CB342",
"#C0CA33",
"#FDD835",
"#FFB300",
"#FB8C00",
"#F4511E",
"#6D4C41",
"#757575",
"#546E7A",
"#b71c1c",
"#880E4F",
"#4A148C",
"#311B92",
"#1A237E",
"#0D47A1",
"#01579B",
"#006064",
"#004D40",
"#1B5E20",
"#33691E",
"#827717",
"#F57F17",
"#FF6F00",
"#E65100",
"#BF360C",
"#3E2723",
"#212121",
"#263238",
];
export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
@@ -66,7 +106,7 @@ export const languages: Language[] = [
// { code: 'lv', name: 'Latviešu' }, // Latvian
// { code: 'lt', name: 'Lietuvių' }, // Lithuanian
{ code: 'hu', name: 'Magyar' }, // Hungarian
// { code: 'mk', name: 'Македонски' }, // Macedonian
{ code: 'mk', name: 'Македонски' }, // Macedonian
// { code: 'ms', name: 'Bahasa Melayu' }, // Malay
{ code: 'nl', name: 'Nederlands' }, // Dutch
{ code: 'ja', name: '日本語' }, // Japanese
@@ -75,7 +115,7 @@ export const languages: Language[] = [
{ code: 'pl', name: 'Polski' }, // Polish
{ code: 'pt', name: 'Português' }, // Portuguese
// { code: 'pt-BR', name: 'Português (Brazil)' }, // Portuguese (Brazil)
// { code: 'ro', name: 'Română' }, // Romanian
{ code: 'ro', name: 'Română' }, // Romanian
{ code: 'ru', name: 'Русский' }, // Russian
// { code: 'sk', name: 'Slovenčina' }, // Slovak
{ code: 'sl', name: 'Slovenščina' }, // Slovenian

View File

@@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { NgxEchartsModule } from 'ngx-echarts';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './components/app/app.component';
@@ -26,16 +27,17 @@ import { LiquidMasterPageComponent } from './components/liquid-master-page/liqui
import { AboutComponent } from './components/about/about.component';
import { TelevisionComponent } from './components/television/television.component';
import { StatisticsComponent } from './components/statistics/statistics.component';
import { ChartistComponent } from './components/statistics/chartist.component';
import { BlockchainBlocksComponent } from './components/blockchain-blocks/blockchain-blocks.component';
import { BlockchainComponent } from './components/blockchain/blockchain.component';
import { FooterComponent } from './components/footer/footer.component';
import { AudioService } from './services/audio.service';
import { MempoolBlockComponent } from './components/mempool-block/mempool-block.component';
import { FeeDistributionGraphComponent } from './components/fee-distribution-graph/fee-distribution-graph.component';
import { IncomingTransactionsGraphComponent } from './components/incoming-transactions-graph/incoming-transactions-graph.component';
import { TimeSpanComponent } from './components/time-span/time-span.component';
import { SeoService } from './services/seo.service';
import { MempoolGraphComponent } from './components/mempool-graph/mempool-graph.component';
import { LbtcPegsGraphComponent } from './components/lbtc-pegs-graph/lbtc-pegs-graph.component';
import { AssetComponent } from './components/asset/asset.component';
import { AssetsComponent } from './assets/assets.component';
import { StatusViewComponent } from './components/status-view/status-view.component';
@@ -45,7 +47,7 @@ import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
import { FeesBoxComponent } from './components/fees-box/fees-box.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faDatabase, faExchangeAlt, faInfoCircle,
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faDatabase, faExchangeAlt, faInfoCircle,
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
import { ApiDocsComponent } from './components/api-docs/api-docs.component';
import { CodeTemplateComponent } from './components/api-docs/code-template.component';
@@ -78,11 +80,12 @@ import { SponsorComponent } from './components/sponsor/sponsor.component';
TimeSpanComponent,
AddressLabelsComponent,
MempoolBlocksComponent,
ChartistComponent,
FooterComponent,
MempoolBlockComponent,
FeeDistributionGraphComponent,
IncomingTransactionsGraphComponent,
MempoolGraphComponent,
LbtcPegsGraphComponent,
AssetComponent,
AssetsComponent,
MinerComponent,
@@ -106,6 +109,9 @@ import { SponsorComponent } from './components/sponsor/sponsor.component';
NgbTypeaheadModule,
FontAwesomeModule,
SharedModule,
NgxEchartsModule.forRoot({
echarts: () => import('echarts')
})
],
providers: [
ElectrsApiService,
@@ -134,6 +140,7 @@ export class AppModule {
library.addIcons(faLink);
library.addIcons(faBolt);
library.addIcons(faTint);
library.addIcons(faFilter);
library.addIcons(faAngleDown);
library.addIcons(faAngleUp);
library.addIcons(faExchangeAlt);

View File

@@ -59,7 +59,7 @@
<span style="float: left;" class="d-none d-md-block">{{ tx.id }}</span>
</a>
<div class="float-right">
{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
&lrm;{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
</div>
<div class="clearfix"></div>
</div>

View File

@@ -20,7 +20,7 @@
<tr>
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
<td>
{{ block.time | date:'yyyy-MM-dd HH:mm' }}
&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>
</div>
@@ -58,7 +58,7 @@
<span style="float: left;" class="d-none d-md-block">{{ tx.id }}</span>
</a>
<div class="float-right">
{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
&lrm;{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
</div>
<div class="clearfix"></div>
</div>

View File

@@ -12,7 +12,7 @@
<tbody *ngIf="(trades$ | async) as trades; else loadingTmpl">
<tr *ngFor="let trade of trades;">
<td>
{{ trade.trade_date | date:'yyyy-MM-dd HH:mm' }}
&lrm;{{ trade.trade_date | date:'yyyy-MM-dd HH:mm' }}
</td>
<td *ngIf="view === 'all'">
<ng-container *ngIf="(trade._market || market).rtype === 'fiat'; else priceCrypto"><span class="green-color">{{ trade.price | currency: (trade._market || market).rsymbol }}</span></ng-container>

View File

@@ -32,7 +32,7 @@
<tr>
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
<td>
{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }}
&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>
</div>

View File

@@ -1,8 +1,8 @@
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { BisqTransaction, BisqOutput } from '../bisq.interfaces';
import { merge, Observable } from 'rxjs';
import { switchMap, map, tap, filter } from 'rxjs/operators';
import { Observable, Subscription } from 'rxjs';
import { switchMap, map, tap } from 'rxjs/operators';
import { BisqApiService } from '../bisq-api.service';
import { SeoService } from 'src/app/services/seo.service';
import { FormGroup, FormBuilder } from '@angular/forms';
@@ -16,7 +16,7 @@ import { WebsocketService } from 'src/app/services/websocket.service';
styleUrls: ['./bisq-transactions.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BisqTransactionsComponent implements OnInit {
export class BisqTransactionsComponent implements OnInit, OnDestroy {
transactions$: Observable<[BisqTransaction[], number]>;
page = 1;
itemsPerPage = 50;
@@ -25,6 +25,7 @@ export class BisqTransactionsComponent implements OnInit {
loadingItems: number[];
radioGroupForm: FormGroup;
types: string[] = [];
radioGroupSubscription: Subscription;
txTypeOptions: IMultiSelectOption[] = [
{ id: 1, name: $localize`Asset listing fee` },
@@ -90,52 +91,39 @@ export class BisqTransactionsComponent implements OnInit {
this.paginationMaxSize = 3;
}
this.transactions$ = merge(
this.route.queryParams
.pipe(
filter((queryParams) => {
const newPage = parseInt(queryParams.page, 10);
const types = queryParams.types;
if (newPage !== this.page || types !== this.types.map((type) => this.txTypes.indexOf(type) + 1).join(',')) {
return true;
}
return false;
}),
tap((queryParams) => {
if (queryParams.page) {
const newPage = parseInt(queryParams.page, 10);
this.page = newPage;
} else {
this.page = 1;
}
if (queryParams.types) {
const types = queryParams.types.split(',').map((str: string) => parseInt(str, 10));
this.types = types.map((id: number) => this.txTypes[id - 1]);
this.radioGroupForm.get('txTypes').setValue(types, { emitEvent: false });
} else {
this.types = [];
this.radioGroupForm.get('txTypes').setValue(this.txTypesDefaultChecked, { emitEvent: false });
}
this.cd.markForCheck();
})
),
this.radioGroupForm.valueChanges
.pipe(
tap((data) => {
this.types = data.txTypes.map((id: number) => this.txTypes[id - 1]);
if (this.types.length === this.txTypes.length) {
this.types = [];
}
this.page = 1;
this.typesChanged(data.txTypes);
this.cd.markForCheck();
})
),
)
this.transactions$ = this.route.queryParams
.pipe(
tap((queryParams) => {
if (queryParams.page) {
const newPage = parseInt(queryParams.page, 10);
this.page = newPage;
} else {
this.page = 1;
}
if (queryParams.types) {
const types = queryParams.types.split(',').map((str: string) => parseInt(str, 10));
this.types = types.map((id: number) => this.txTypes[id - 1]);
this.radioGroupForm.get('txTypes').setValue(types, { emitEvent: false });
} else {
this.types = [];
this.radioGroupForm.get('txTypes').setValue([], { emitEvent: false });
}
this.cd.markForCheck();
}),
switchMap(() => this.bisqApiService.listTransactions$((this.page - 1) * this.itemsPerPage, this.itemsPerPage, this.types)),
map((response) => [response.body, parseInt(response.headers.get('x-total-count'), 10)])
);
this.radioGroupSubscription = this.radioGroupForm.valueChanges
.subscribe((data) => {
this.types = data.txTypes.map((id: number) => this.txTypes[id - 1]);
if (this.types.length === this.txTypes.length) {
this.types = [];
}
this.page = 1;
this.typesChanged(data.txTypes);
this.cd.markForCheck();
});
}
pageChange(page: number) {
@@ -144,8 +132,6 @@ export class BisqTransactionsComponent implements OnInit {
queryParams: { page: page },
queryParamsHandling: 'merge',
});
// trigger queryParams change
this.page = -1;
}
typesChanged(types: number[]) {
@@ -172,4 +158,8 @@ export class BisqTransactionsComponent implements OnInit {
onResize(event: any) {
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
}
ngOnDestroy(): void {
this.radioGroupSubscription.unsubscribe();
}
}

View File

@@ -71,7 +71,7 @@ interface BisqScriptPubKey {
addresses: string[];
asm: string;
hex: string;
reqSigs: number;
reqSigs?: number;
type: string;
}

View File

@@ -50,7 +50,7 @@
<img class="image" src="/resources/profile/foundry.svg" />
<span>Foundry</span>
</a>
<a href="https://unchained-capital.com/" target="_blank" title="Unchained">
<a href="https://unchained.com/" target="_blank" title="Unchained">
<img class="image" src="/resources/profile/unchained.svg" />
<span>Unchained</span>
</a>
@@ -126,6 +126,10 @@
<img class="image" src="/resources/profile/blixt.png" />
<span>Blixt</span>
</a>
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="Zeus">
<img class="image" src="/resources/profile/zeus.png" />
<span>Zeus</span>
</a>
<a href="https://github.com/vulpemventures/marina" target="_blank" title="Marina Wallet">
<img class="image" src="/resources/profile/marina.svg" />
<span>Marina</span>
@@ -138,10 +142,6 @@
<img class="image" src="/resources/profile/blw.png" />
<span>BLW</span>
</a>
<a href="https://github.com/pxsocs/warden" target="_blank" title="WARden Portfolio">
<img class="image" src="/resources/profile/warden.jpg" />
<span>WARden</span>
</a>
</div>
</div>

View File

@@ -18,6 +18,10 @@
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody>
<tr *ngIf="addressInfo && addressInfo.unconfidential">
<td i18n="address.unconfidential">Unconfidential</td>
<td><a [routerLink]="['/address/' | relativeUrl, addressInfo.unconfidential]">{{ addressInfo.unconfidential }}</a> <app-clipboard [text]="addressInfo.unconfidential"></app-clipboard></td>
</tr>
<ng-template [ngIf]="!address.electrum">
<tr>
<td i18n="address.total-received">Total received</td>

View File

@@ -9,6 +9,7 @@ import { AudioService } from 'src/app/services/audio.service';
import { ApiService } from 'src/app/services/api.service';
import { of, merge, Subscription, Observable } from 'rxjs';
import { SeoService } from 'src/app/services/seo.service';
import { AddressInformation } from 'src/app/interfaces/node-api.interface';
@Component({
selector: 'app-address',
@@ -27,6 +28,7 @@ export class AddressComponent implements OnInit, OnDestroy {
error: any;
mainSubscription: Subscription;
addressLoadingStatus$: Observable<number>;
addressInfo: null | AddressInformation = null;
totalConfirmedTxCount = 0;
loadedConfirmedTxCount = 0;
@@ -67,8 +69,12 @@ export class AddressComponent implements OnInit, OnDestroy {
this.address = null;
this.isLoadingTransactions = true;
this.transactions = null;
this.addressInfo = null;
document.body.scrollTo(0, 0);
this.addressString = params.get('id') || '';
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) {
this.addressString = this.addressString.toLowerCase();
}
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
return merge(
@@ -92,10 +98,20 @@ export class AddressComponent implements OnInit, OnDestroy {
)
.pipe(
filter((address) => !!address),
tap((address: Address) => {
if (this.stateService.network === 'liquid' && /^([m-zA-HJ-NP-Z1-9]{26,35}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[a-km-zA-HJ-NP-Z1-9]{80})$/.test(address.address)) {
this.apiService.validateAddress$(address.address)
.subscribe((addressInfo) => {
this.addressInfo = addressInfo;
this.websocketService.startTrackAddress(addressInfo.unconfidential);
});
} else {
this.websocketService.startTrackAddress(address.address);
}
}),
switchMap((address) => {
this.address = address;
this.updateChainStats();
this.websocketService.startTrackAddress(address.address);
this.isLoadingAddress = false;
this.isLoadingTransactions = true;
return this.electrsApiService.getAddressTransactions$(address.address);
@@ -126,7 +142,13 @@ export class AddressComponent implements OnInit, OnDestroy {
this.tempTransactions[this.timeTxIndexes[index]].firstSeen = time;
});
this.tempTransactions.sort((a, b) => {
return b.status.block_time - a.status.block_time || b.firstSeen - a.firstSeen;
if (b.status.confirmed) {
if (b.status.block_height === a.status.block_height) {
return b.status.block_time - a.status.block_time;
}
return b.status.block_height - a.status.block_height;
}
return b.firstSeen - a.firstSeen;
});
this.transactions = this.tempTransactions;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,20 @@
<div class="code">
<ul ngbNav #navCodeTemplate="ngbNav" class="nav-tabs code-tab">
<li ngbNavItem *ngIf="code.codeSample.curl">
<li ngbNavItem *ngIf="code.codeTemplate.curl && method !== 'websocket'">
<a ngbNavLink>cURL</a>
<ng-template ngbNavContent>
<div class="subtitle"><ng-container i18n="API Docs code example">Code Example</ng-container> <app-clipboard [text]="wrapCurl(code.codeSample.curl)"></app-clipboard></div>
<pre><code [innerText]="wrapCurl(code.codeSample.curl)"></code></pre>
<div class="subtitle"><ng-container i18n="API Docs code example">Code Example</ng-container> <app-clipboard [text]="wrapCurlTemplate(code)"></app-clipboard></div>
<pre><code [innerText]="wrapCurlTemplate(code)"></code></pre>
</ng-template>
</li>
<li ngbNavItem>
<a ngbNavLink>CommonJS</a>
<ng-template ngbNavContent>
<div class="subtitle"><ng-container i18n="API Docs code example">Code Example</ng-container> <app-clipboard [text]="wrapCommonJS(code.codeSample.commonJS)"></app-clipboard></div>
<div class="subtitle"><ng-container i18n="API Docs code example">Code Example</ng-container> <app-clipboard [text]="wrapCommonJS(code)"></app-clipboard></div>
<div class="links">
<a [href]="npmGithubLink()" target="_blank">github repository</a>
</div>
<pre><code [innerText]="wrapCommonJS(code.codeSample.commonJS)"></code></pre>
<pre><code [innerText]="wrapCommonJS(code)"></code></pre>
</ng-template>
</li>
<li ngbNavItem>
@@ -26,14 +26,14 @@
<a [href]="npmModuleLink()" target="_blank">npm package</a>
</div>
<pre><code [innerText]="wrapImportTemplate()"></code></pre>
<div class="subtitle"><ng-container i18n="API Docs code example">Code Example</ng-container> <app-clipboard [text]="wrapESmodule(code.codeSample.esModule)"></app-clipboard></div>
<pre><code [innerText]="wrapESmodule(code.codeSample.esModule)"></code></pre>
<div class="subtitle"><ng-container i18n="API Docs code example">Code Example</ng-container> <app-clipboard [text]="wrapEsModule(code)"></app-clipboard></div>
<pre><code [innerText]="wrapEsModule(code)"></code></pre>
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="navCodeTemplate"></div>
<div *ngIf="code.responseSample" class="response">
<div class="subtitle"><ng-container i18n="API Docs API response">Response</ng-container> <app-clipboard [text]="code.responseSample"></app-clipboard></div>
<pre><code [innerText]="code.responseSample"></code></pre>
<div *ngIf="code.codeTemplate && wrapResponse(code) !== ''" class="response">
<div class="subtitle"><ng-container i18n="API Docs API response">Response</ng-container> <app-clipboard [text]="wrapResponse(code)"></app-clipboard></div>
<pre><code [innerText]="wrapResponse(code)"></code></pre>
</div>
</div>
</div>

View File

@@ -1,32 +1,32 @@
import { Component, Input } from '@angular/core';
import { Component, Input, OnInit } from '@angular/core';
import { Env, StateService } from 'src/app/services/state.service';
@Component({
selector: 'app-code-template',
templateUrl: './code-template.component.html',
styleUrls: ['./code-template.component.scss']
})
export class CodeTemplateComponent {
export class CodeTemplateComponent implements OnInit {
@Input() network: string;
@Input() layer: string;
@Input() code: {
codeSample: {
esModule: string;
commonJS: string;
curl: string;
},
responseSample: string;
};
hostname = document.location.hostname;
@Input() code: any;
@Input() hostname: string;
@Input() method: 'get' | 'post' | 'websocket' = 'get';
env: Env;
constructor(
private stateService: StateService,
) { }
ngOnInit(): void {
this.env = this.stateService.env;
}
npmGithubLink(){
let npmLink = `https://github.com/mempool/mempool.js`;
if (this.layer === 'bisq') {
if (this.network === 'bisq') {
npmLink = `https://github.com/mempool/mempool.js/tree/main/npm-bisq-js`;
}
if (this.layer === 'liquid') {
if (this.network === 'liquid') {
npmLink = `https://github.com/mempool/mempool.js/tree/main/npm-liquid-js`;
}
return npmLink;
@@ -34,67 +34,153 @@ export class CodeTemplateComponent {
npmModuleLink() {
let npmLink = `https://www.npmjs.org/package/@mempool/mempool.js`;
if (this.layer === 'bisq') {
if (this.network === 'bisq') {
npmLink = `https://www.npmjs.org/package/@mempool/bisq.js`;
}
if (this.layer === 'liquid') {
if (this.network === 'liquid') {
npmLink = `https://www.npmjs.org/package/@mempool/liquid.js`;
}
return npmLink;
}
normalizeCodeHostname(code: string) {
let codeText: string;
if (this.network === 'bisq' || this.network === 'liquid'){
codeText = code.replace('%{1}', this.network);
}else{
codeText = code.replace('%{1}', 'bitcoin');
normalizeHostsESModule(codeText: string) {
if (this.env.BASE_MODULE === 'mempool') {
if (['liquid', 'bisq'].includes(this.network)) {
codeText = codeText.replace('%{0}', this.network);
} else {
codeText = codeText.replace('%{0}', 'bitcoin');
}
if(['', 'main', 'liquid', 'bisq'].includes(this.network)) {
codeText = codeText.replace('mempoolJS();', `mempoolJS({
hostname: '${document.location.hostname}'
});`);
} else {
codeText = codeText.replace('mempoolJS();', `mempoolJS({
hostname: '${document.location.hostname}',
network: '${this.network}'
});`);
}
}
if (this.env.BASE_MODULE === 'bisq') {
codeText = codeText.replace('} = mempoolJS();', ` = bisqJS();`);
codeText = codeText.replace('{ %{0}: ', '');
}
if (this.env.BASE_MODULE === 'liquid') {
codeText = codeText.replace('} = mempoolJS();', ` = liquidJS();`);
codeText = codeText.replace('{ %{0}: ', '');
}
return codeText;
}
wrapESmodule(code: string) {
let codeText = this.normalizeCodeHostname(code);
if (this.network && this.network !== 'mainnet') {
codeText = codeText.replace('mempoolJS();', `mempoolJS({
hostname: '${this.hostname}/${this.network}'
});` );
normalizeHostsCommonJS(codeText: string) {
if (this.env.BASE_MODULE === 'mempool') {
if (['liquid', 'bisq'].includes(this.network)) {
codeText = codeText.replace('%{0}', this.network);
} else {
codeText = codeText.replace('%{0}', 'bitcoin');
}
if(['', 'main', 'liquid', 'bisq'].includes(this.network)) {
codeText = codeText.replace('mempoolJS();', `mempoolJS({
hostname: '${document.location.hostname}'
});`);
} else {
codeText = codeText.replace('mempoolJS();', `mempoolJS({
hostname: '${document.location.hostname}',
network: '${this.network}'
});`);
}
}
let importText = `import mempoolJS from "@mempool/mempool.js";`;
if (this.layer === 'bisq') {
importText = `import bisqJS from "@mempool/bisq.js";`;
}
if (this.layer === 'liquid') {
importText = `import liquidJS from "@mempool/liquid.js";`;
if (this.env.BASE_MODULE === 'bisq') {
codeText = codeText.replace('} = mempoolJS();', ` = bisqJS();`);
codeText = codeText.replace('{ %{0}: ', '');
}
return `${importText}
if (this.env.BASE_MODULE === 'liquid') {
codeText = codeText.replace('} = mempoolJS();', ` = liquidJS();`);
codeText = codeText.replace('{ %{0}: ', '');
}
return codeText;
}
wrapEsModule(code: any) {
let codeText: string;
if (code.codeTemplate) {
codeText = this.normalizeHostsESModule(code.codeTemplate.esModule);
if(this.network === '' || this.network === 'main') {
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleMainnet.esModule);
}
if (this.network === 'testnet') {
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleTestnet.esModule);
}
if (this.network === 'signet') {
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleSignet.esModule);
}
if (this.network === 'liquid') {
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleLiquid.esModule);
}
if (this.network === 'bisq') {
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleBisq.esModule);
}
let importText = `import mempoolJS from "@mempool/mempool.js";`;
if (this.env.BASE_MODULE === 'bisq') {
importText = `import bisqJS from "@mempool/bisq.js";`;
}
if (this.env.BASE_MODULE === 'liquid') {
importText = `import liquidJS from "@mempool/liquid.js";`;
}
return `${importText}
const init = async () => {
${codeText}
};
init();`;
}
}
wrapCommonJS(code: string) {
let codeText = this.normalizeCodeHostname(code);
wrapCommonJS(code: any) {
let codeText: string;
if (code.codeTemplate) {
codeText = this.normalizeHostsCommonJS(code.codeTemplate.commonJS);
if (this.network && this.network !== 'mainnet') {
codeText = codeText.replace('mempoolJS();', `mempoolJS({
hostname: '${this.hostname}/${this.network}'
});` );
}
if(this.network === '' || this.network === 'main') {
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleMainnet.esModule);
}
if (this.network === 'testnet') {
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleTestnet.esModule);
}
if (this.network === 'signet') {
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleSignet.esModule);
}
if (this.network === 'liquid') {
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleLiquid.esModule);
}
if (this.network === 'bisq') {
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleBisq.esModule);
}
let importText = `<script src="https://mempool.space/mempool.js"></script>`;
if (this.layer === 'bisq') {
importText = `<script src="https://bisq.markets/bisq.js"></script>`;
}
if (this.layer === 'liquid') {
importText = `<script src="https://liquid.network/liquid.js"></script>`;
}
return `<!DOCTYPE html>
let importText = `<script src="https://mempool.space/mempool.js"></script>`;
if (this.env.BASE_MODULE === 'bisq') {
importText = `<script src="https://bisq.markets/bisq.js"></script>`;
}
if (this.env.BASE_MODULE === 'liquid') {
importText = `<script src="https://liquid.network/liquid.js"></script>`;
}
let resultHtml = '<pre id="result"></pre>';
if (this.method === 'websocket') {
resultHtml = `<pre id="result-blocks"></pre>
<pre id="result-mempool-info"></pre>
<pre id="result-transactions"></pre>
<pre id="result-mempool-blocks"></pre>`;
}
return `<!DOCTYPE html>
<html>
<head>
${importText}
@@ -105,14 +191,11 @@ init();`;
init();
</script>
</head>
<body></body>
<body>
${resultHtml}
</body>
</html>`;
}
wrapCurl(code: string) {
if (this.network && this.network !== 'mainnet') {
return code.replace('mempool.space/', `mempool.space/${this.network}/`);
}
return code;
}
wrapImportTemplate() {
@@ -123,7 +206,7 @@ npm install @mempool/mempool.js --save
# yarn
yarn add @mempool/mempool.js`;
if (this.layer === 'bisq') {
if (this.env.BASE_MODULE === 'bisq') {
importTemplate = `# npm
npm install @mempool/bisq.js --save
@@ -131,7 +214,7 @@ npm install @mempool/bisq.js --save
yarn add @mempool/bisq.js`;
}
if (this.layer === 'liquid') {
if (this.env.BASE_MODULE === 'liquid') {
importTemplate = `# npm
npm install @mempool/liquid.js --save
@@ -142,4 +225,78 @@ yarn add @mempool/liquid.js`;
return importTemplate;
}
wrapCurlTemplate(code: any) {
if (code.codeTemplate) {
if (this.network === 'testnet') {
return this.replaceCurlPlaceholder(code.codeTemplate.curl, code.codeSampleTestnet);
}
if (this.network === 'signet') {
return this.replaceCurlPlaceholder(code.codeTemplate.curl, code.codeSampleSignet);
}
if (this.network === 'liquid') {
return this.replaceCurlPlaceholder(code.codeTemplate.curl, code.codeSampleLiquid);
}
if (this.network === 'bisq') {
return this.replaceCurlPlaceholder(code.codeTemplate.curl, code.codeSampleBisq);
}
if (this.network === '' || this.network === 'main') {
return this.replaceCurlPlaceholder(code.codeTemplate.curl, code.codeSampleMainnet);
}
}
}
wrapResponse(code: any) {
if (this.method === 'websocket') {
return '';
}
if (this.network === 'testnet') {
return code.codeSampleTestnet.response;
}
if (this.network === 'signet') {
return code.codeSampleSignet.response;
}
if (this.network === 'liquid') {
return code.codeSampleLiquid.response;
}
if (this.network === 'bisq') {
return code.codeSampleBisq.response;
}
return code.codeSampleMainnet.response;
}
replaceJSPlaceholder(text: string, code: any) {
for (let index = 0; index < code.length; index++) {
const textReplace = code[index];
const indexNumber = index + 1;
text = text.replace('%{' + indexNumber + '}', textReplace);
}
return text;
}
replaceCurlPlaceholder(curlText: any, code: any) {
let text = curlText;
for (let index = 0; index < code.curl.length; index++) {
const textReplace = code.curl[index];
const indexNumber = index + 1;
text = text.replace('%{' + indexNumber + '}', textReplace);
}
if (this.env.BASE_MODULE === 'mempool') {
if (this.network === 'main' || this.network === '') {
if (this.method === 'post') {
return `curl -X POST -sSLd "${text}"`;
}
return `curl -sSL "${this.hostname}${text}"`;
}
if (this.method === 'post') {
text = text.replace('/api', `/${this.network}/api`);
return `curl -X POST -sSLd "${text}"`;
}
return `curl -sSL "${this.hostname}/${this.network}${text}"`;
}
if (this.env.BASE_MODULE !== 'mempool') {
return `curl -sSL "${this.hostname}${text}"`;
}
}
}

View File

@@ -39,7 +39,13 @@ export class AppComponent implements OnInit {
ngOnInit() {
this.router.events.subscribe((val) => {
if (val instanceof NavigationEnd) {
this.link.setAttribute('href', 'https://mempool.space' + (this.location.path() === '/' ? '' : this.location.path()));
let domain = 'mempool.space';
if (this.stateService.env.BASE_MODULE === 'liquid') {
domain = 'liquid.network';
} else if (this.stateService.env.BASE_MODULE === 'bisq') {
domain = 'bisq.markets';
}
this.link.setAttribute('href', 'https://' + domain + this.location.path());
}
});
}

View File

@@ -136,7 +136,13 @@ export class AssetComponent implements OnInit, OnDestroy {
this.tempTransactions[this.timeTxIndexes[index]].firstSeen = time;
});
this.tempTransactions.sort((a, b) => {
return b.status.block_time - a.status.block_time || b.firstSeen - a.firstSeen;
if (b.status.confirmed) {
if (b.status.block_height === a.status.block_height) {
return b.status.block_time - a.status.block_time;
}
return b.status.block_height - a.status.block_height;
}
return b.firstSeen - a.firstSeen;
});
this.transactions = this.tempTransactions;

View File

@@ -53,7 +53,7 @@
<tr>
<td i18n="block.timestamp">Timestamp</td>
<td>
{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
<i class="symbol">(<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>)</i>
</div>
@@ -61,11 +61,11 @@
</tr>
<tr>
<td i18n="block.size">Size</td>
<td [innerHTML]="block.size | bytes: 2"></td>
<td [innerHTML]="'&lrm;' + (block.size | bytes: 2)"></td>
</tr>
<tr>
<td i18n="block.weight">Weight</td>
<td [innerHTML]="block.weight | wuBytes: 2"></td>
<td [innerHTML]="'&lrm;' + (block.weight | wuBytes: 2)"></td>
</tr>
</tbody>
</table>
@@ -162,13 +162,13 @@
</div>
<div #blockTxTitle id="block-tx-title" class="block-tx-title">
<h2>
<h2 class="float-left">
<ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
</h2>
<ngb-pagination class="pagination-container" [collectionSize]="block.tx_count" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page, blockTxTitle)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
<ngb-pagination class="pagination-container float-right" [collectionSize]="block.tx_count" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page, blockTxTitle)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
</div>
<div class="clearfix"></div>

View File

@@ -12,7 +12,7 @@
<div class="fee-span">
{{ block.feeRange[1] | number:feeRounding }} - {{ block.feeRange[block.feeRange.length - 1] | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
</div>
<div class="block-size" [innerHTML]="block.size | bytes: 2">&lrm;</div>
<div class="block-size" [innerHTML]="'&lrm;' + (block.size | bytes: 2)"></div>
<div class="transaction-count">
<ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>

View File

@@ -1,9 +1,5 @@
<div style="height: 225px;" *ngIf="mempoolVsizeFeesData; else loadingFees">
<app-chartist
[data]="mempoolVsizeFeesData"
[type]="'Line'"
[options]="mempoolVsizeFeesOptions">
</app-chartist>
<div class="fee-distribution-chart" *ngIf="mempoolVsizeFeesOptions; else loadingFees">
<div echarts [initOpts]="mempoolVsizeFeesInitOptions" [options]="mempoolVsizeFeesOptions"></div>
</div>
<ng-template #loadingFees>

View File

@@ -1,70 +1,83 @@
import { Component, Input, OnChanges, ChangeDetectionStrategy } from '@angular/core';
import * as Chartist from '@mempool/chartist';
import { OnChanges } from '@angular/core';
import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-fee-distribution-graph',
templateUrl: './fee-distribution-graph.component.html',
styleUrls: ['./fee-distribution-graph.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FeeDistributionGraphComponent implements OnChanges {
@Input() feeRange;
export class FeeDistributionGraphComponent implements OnInit, OnChanges {
@Input() data: any;
@Input() height: number | string = 210;
@Input() top: number | string = 20;
@Input() right: number | string = 22;
@Input() left: number | string = 30;
mempoolVsizeFeesData: any;
mempoolVsizeFeesOptions: any;
mempoolVsizeFeesInitOptions = {
renderer: 'svg'
};
feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
250, 300, 350, 400, 500];
constructor() { }
constructor(
) { }
ngOnChanges() {
this.mempoolVsizeFeesOptions = {
showArea: true,
showLine: true,
fullWidth: true,
showPoint: true,
low: 0,
axisY: {
showLabel: false,
offset: 0
},
axisX: {
showGrid: true,
showLabel: false,
offset: 0
},
plugins: [
Chartist.plugins.ctPointLabels({
textAnchor: 'middle',
labelInterpolationFnc: (value) => Math.round(value)
})
]
};
const fees = this.feeRange;
const series = [];
for (let i = 0; i < this.feeLevels.length; i++) {
let total = 0;
// for (let j = 0; j < fees.length; j++) {
for (const fee of fees) {
if (i === this.feeLevels.length - 1) {
if (fee >= this.feeLevels[i]) {
total += 1;
}
} else if (fee >= this.feeLevels[i] && fee < this.feeLevels[i + 1]) {
total += 1;
}
}
series.push(total);
}
this.mempoolVsizeFeesData = {
series: [fees],
labels: fees.map((d, i) => i)
};
ngOnInit() {
this.mountChart();
}
ngOnChanges() {
this.mountChart();
}
mountChart() {
this.mempoolVsizeFeesOptions = {
grid: {
height: '210',
right: '20',
top: '22',
left: '30',
},
xAxis: {
type: 'category',
boundaryGap: false,
},
yAxis: {
type: 'value',
splitLine: {
lineStyle: {
type: 'dotted',
color: '#ffffff66',
opacity: 0.25,
}
}
},
series: [{
data: this.data,
type: 'line',
label: {
show: true,
position: 'top',
color: '#ffffff',
textShadowBlur: 0,
formatter: (label: any) => {
return Math.floor(label.data);
},
},
smooth: true,
lineStyle: {
color: '#D81B60',
width: 4,
},
itemStyle: {
color: '#b71c1c',
borderWidth: 10,
borderMiterLimit: 10,
opacity: 1,
},
areaStyle: {
color: '#D81B60',
opacity: 1,
}
}]
};
}
}

View File

@@ -0,0 +1 @@
<div class="echarts" echarts [initOpts]="mempoolStatsChartInitOption" [options]="mempoolStatsChartOption"></div>

View File

@@ -0,0 +1,188 @@
import { Component, OnInit, Input, Inject, LOCALE_ID, ChangeDetectionStrategy } from '@angular/core';
import { formatDate } from '@angular/common';
import { EChartsOption } from 'echarts';
import { OnChanges } from '@angular/core';
import { StorageService } from 'src/app/services/storage.service';
@Component({
selector: 'app-incoming-transactions-graph',
templateUrl: './incoming-transactions-graph.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IncomingTransactionsGraphComponent implements OnInit, OnChanges {
@Input() data: any;
@Input() theme: string;
@Input() height: number | string = '200';
@Input() right: number | string = '10';
@Input() top: number | string = '20';
@Input() left: number | string = '0';
@Input() template: ('widget' | 'advanced') = 'widget';
mempoolStatsChartOption: EChartsOption = {};
mempoolStatsChartInitOption = {
renderer: 'svg'
};
windowPreference: string;
constructor(
@Inject(LOCALE_ID) private locale: string,
private storageService: StorageService,
) { }
ngOnChanges(): void {
this.windowPreference = this.storageService.getValue('graphWindowPreference');
this.mountChart();
}
ngOnInit(): void {
this.mountChart();
}
mountChart(): void {
this.mempoolStatsChartOption = {
grid: {
height: this.height,
right: this.right,
top: this.top,
left: this.left,
},
animation: false,
dataZoom: [{
type: 'inside',
realtime: true,
zoomOnMouseWheel: (this.template === 'advanced') ? true : false,
maxSpan: 100,
minSpan: 10,
}, {
show: (this.template === 'advanced') ? true : false,
type: 'slider',
brushSelect: false,
realtime: true,
selectedDataBackground: {
lineStyle: {
color: '#fff',
opacity: 0.45,
},
areaStyle: {
opacity: 0,
}
}
}],
tooltip: {
trigger: 'axis',
position: (pos, params, el, elRect, size) => {
const obj = { top: -20 };
obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80;
return obj;
},
extraCssText: `width: ${(['2h', '24h'].includes(this.windowPreference) || this.template === 'widget') ? '125px' : '135px'};
background: transparent;
border: none;
box-shadow: none;`,
axisPointer: {
type: 'line',
},
formatter: (params: any) => {
const colorSpan = (color: string) => `<span class="indicator" style="background-color: ` + color + `"></span>`;
let itemFormatted = '<div class="title">' + params[0].axisValue + '</div>';
params.map((item: any, index: number) => {
if (index < 26) {
itemFormatted += `<div class="item">
<div class="indicator-container">${colorSpan(item.color)}</div>
<div class="grow"></div>
<div class="value">${item.value} <span class="symbol">vB/s</span></div>
</div>`;
}
});
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}">${itemFormatted}</div>`;
}
},
xAxis: {
type: 'category',
axisLabel: {
align: 'center',
fontSize: 11,
lineHeight: 12
},
data: this.data.labels.map((value: any) => `${formatDate(value, 'M/d', this.locale)}\n${formatDate(value, 'H:mm', this.locale)}`),
},
yAxis: {
type: 'value',
axisLabel: {
fontSize: 11,
},
splitLine: {
lineStyle: {
type: 'dotted',
color: '#ffffff66',
opacity: 0.25,
}
}
},
series: [
{
data: this.data.series[0],
type: 'line',
smooth: false,
showSymbol: false,
lineStyle: {
width: 3,
},
markLine: {
silent: true,
symbol: 'none',
lineStyle: {
color: '#fff',
opacity: 1,
width: 2,
},
data: [{
yAxis: 1667,
label: {
show: false,
color: '#ffffff',
}
}],
}
},
],
visualMap: {
show: false,
top: 50,
right: 10,
pieces: [{
gt: 0,
lte: 1667,
color: '#7CB342'
},
{
gt: 1667,
lte: 2000,
color: '#FDD835'
},
{
gt: 2000,
lte: 2500,
color: '#FFB300'
},
{
gt: 2500,
lte: 3000,
color: '#FB8C00'
},
{
gt: 3000,
lte: 3500,
color: '#F4511E'
},
{
gt: 3500,
color: '#D81B60'
}],
outOfRange: {
color: '#999'
}
},
};
}
}

View File

@@ -15,7 +15,7 @@
<tbody>
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
<td><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
<td class="d-none d-md-block">{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
<td class="d-none d-md-block">&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
<td><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></td>
<td class="d-none d-lg-block">{{ block.tx_count | number }}</td>
<td>

View File

@@ -0,0 +1 @@
<div class="echarts" echarts [initOpts]="pegsChartInitOption" [options]="pegsChartOptions"></div>

View File

@@ -0,0 +1,139 @@
import { Component, Inject, LOCALE_ID, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
import { formatDate, formatNumber } from '@angular/common';
import { EChartsOption } from 'echarts';
@Component({
selector: 'app-lbtc-pegs-graph',
styles: [`::ng-deep .tx-wrapper-tooltip-chart { width: 135px; }`],
templateUrl: './lbtc-pegs-graph.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LbtcPegsGraphComponent implements OnChanges {
@Input() data: any;
pegsChartOptions: EChartsOption;
height: number | string = '200';
right: number | string = '10';
top: number | string = '20';
left: number | string = '50';
template: ('widget' | 'advanced') = 'widget';
pegsChartOption: EChartsOption = {};
pegsChartInitOption = {
renderer: 'svg'
};
constructor(
@Inject(LOCALE_ID) private locale: string,
) { }
ngOnChanges() {
if (!this.data) {
return;
}
this.pegsChartOptions = this.createChartOptions(this.data.series, this.data.labels);
}
createChartOptions(series: number[], labels: string[]): EChartsOption {
return {
grid: {
height: this.height,
right: this.right,
top: this.top,
left: this.left,
},
animation: false,
dataZoom: [{
type: 'inside',
realtime: true,
zoomOnMouseWheel: (this.template === 'advanced') ? true : false,
maxSpan: 100,
minSpan: 10,
}, {
show: (this.template === 'advanced') ? true : false,
type: 'slider',
brushSelect: false,
realtime: true,
selectedDataBackground: {
lineStyle: {
color: '#fff',
opacity: 0.45,
},
areaStyle: {
opacity: 0,
}
}
}],
tooltip: {
trigger: 'axis',
position: (pos, params, el, elRect, size) => {
const obj = { top: -20 };
obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80;
return obj;
},
extraCssText: `width: ${(this.template === 'widget') ? '125px' : '135px'};
background: transparent;
border: none;
box-shadow: none;`,
axisPointer: {
type: 'line',
},
formatter: (params: any) => {
const colorSpan = (color: string) => `<span class="indicator" style="background-color: #116761;"></span>`;
let itemFormatted = '<div class="title">' + params[0].axisValue + '</div>';
params.map((item: any, index: number) => {
if (index < 26) {
itemFormatted += `<div class="item">
<div class="indicator-container">${colorSpan(item.color)}</div>
<div class="grow"></div>
<div class="value">${formatNumber(item.value, this.locale, '1.2-2')} <span class="symbol">L-BTC</span></div>
</div>`;
}
});
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}">${itemFormatted}</div>`;
}
},
xAxis: {
type: 'category',
axisLabel: {
align: 'center',
fontSize: 11,
lineHeight: 12
},
boundaryGap: false,
data: labels.map((value: any) => `${formatDate(value, 'MMM\ny', this.locale)}`),
},
yAxis: {
type: 'value',
axisLabel: {
fontSize: 11,
},
splitLine: {
lineStyle: {
type: 'dotted',
color: '#ffffff66',
opacity: 0.25,
}
}
},
series: [
{
data: series,
type: 'line',
stack: 'total',
smooth: false,
showSymbol: false,
areaStyle: {
opacity: 0.2,
color: '#116761',
},
lineStyle: {
width: 3,
color: '#116761',
},
},
],
};
}
}

View File

@@ -9,7 +9,7 @@
<div class="box">
<div class="row">
<div class="col-sm">
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody>
<tr>
@@ -40,8 +40,8 @@
</tbody>
</table>
</div>
<div class="col-sm">
<app-fee-distribution-graph [feeRange]="mempoolBlock.feeRange"></app-fee-distribution-graph>
<div class="col-md chart-container">
<app-fee-distribution-graph [data]="mempoolBlock.feeRange" ></app-fee-distribution-graph>
</div>
</div>
</div>

View File

@@ -13,11 +13,8 @@
.fiat {
font-size: 13px;
display: block;
@media (min-width: 992px) {
display: inline-block;
margin-left: 10px;
}
display: inline-block;
margin-left: 10px;
}
.table {
@@ -38,4 +35,11 @@ h1 {
float: left;
margin-right: 10px;
}
}
}
.chart-container{
margin: 20px auto;
@media (min-width: 768px) {
margin: auto;
}
}

View File

@@ -11,7 +11,7 @@
<div class="fee-span">
{{ projectedBlock.feeRange[0] | number:feeRounding }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
</div>
<div class="block-size" [innerHTML]="projectedBlock.blockSize | bytes: 2">&lrm;</div>
<div class="block-size" [innerHTML]="'&lrm;' + (projectedBlock.blockSize | bytes: 2)"></div>
<div class="transaction-count">
<ng-container *ngTemplateOutlet="projectedBlock.nTx === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: projectedBlock.nTx | number}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>

View File

@@ -184,7 +184,8 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
}
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
const blocksAmount = Math.max(2, Math.floor(window.innerWidth / 2 / (this.blockWidth + this.blockPadding)));
const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2;
const blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding)));
while (blocks.length > blocksAmount) {
const block = blocks.pop();
const lastBlock = blocks[blocks.length - 1];

View File

@@ -1,6 +1 @@
<app-chartist
*ngIf="mempoolVsizeFeesData"
[data]="mempoolVsizeFeesData"
[type]="'Line'"
[options]="mempoolVsizeFeesOptions">
</app-chartist>
<div echarts class="echarts" (chartInit)="onChartReady($event)" [initOpts]="mempoolVsizeFeesInitOptions" [options]="mempoolVsizeFeesOptions"></div>

View File

@@ -1,10 +1,13 @@
import { Component, OnInit, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, OnChanges } from '@angular/core';
import { formatDate } from '@angular/common';
import { VbytesPipe } from 'src/app/shared/pipes/bytes-pipe/vbytes.pipe';
import * as Chartist from '@mempool/chartist';
import { formatNumber } from "@angular/common";
import { OptimizedMempoolStats } from 'src/app/interfaces/node-api.interface';
import { StateService } from 'src/app/services/state.service';
import { StorageService } from 'src/app/services/storage.service';
import { EChartsOption } from 'echarts';
import { feeLevels, chartColors } from 'src/app/app.constants';
@Component({
selector: 'app-mempool-graph',
@@ -12,134 +15,344 @@ import { StorageService } from 'src/app/services/storage.service';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MempoolGraphComponent implements OnInit, OnChanges {
@Input() data;
@Input() dateSpan = '2h';
@Input() showLegend = true;
@Input() offsetX = 40;
@Input() small = false;
@Input() data: any[];
@Input() limitFee = 350;
@Input() limitFilterFee = 1;
@Input() height: number | string = 200;
@Input() top: number | string = 20;
@Input() right: number | string = 10;
@Input() left: number | string = 75;
@Input() template: ('widget' | 'advanced') = 'widget';
@Input() showZoom = true;
mempoolVsizeFeesOptions: any;
mempoolVsizeFeesData: any;
isMobile = window.innerWidth <= 767.98;
mempoolVsizeFeesOptions: EChartsOption;
mempoolVsizeFeesInitOptions = {
renderer: 'svg',
};
windowPreference: string;
hoverIndexSerie = 0;
feeLimitIndex: number;
feeLevelsOrdered = [];
chartColorsOrdered = chartColors;
inverted: boolean;
constructor(
private vbytesPipe: VbytesPipe,
private stateService: StateService,
@Inject(LOCALE_ID) private locale: string,
private storageService: StorageService,
@Inject(LOCALE_ID) private locale: string,
) { }
ngOnInit(): void {
let labelHops = !this.showLegend ? 48 : 24;
if (this.small) {
labelHops = labelHops / 2;
}
if (this.isMobile) {
labelHops = 96;
}
const labelInterpolationFnc = (value: any, index: any) => {
switch (this.dateSpan) {
case '2h':
case '24h':
value = formatDate(value, 'HH:mm', this.locale);
break;
case '1w':
value = formatDate(value, 'dd/MM HH:mm', this.locale);
break;
case '1m':
case '3m':
case '6m':
case '1y':
value = formatDate(value, 'dd/MM', this.locale);
}
return index % labelHops === 0 ? value : null;
};
this.mempoolVsizeFeesOptions = {
showArea: true,
showLine: false,
fullWidth: true,
showPoint: false,
stackedLine: !this.inverted,
low: 0,
axisX: {
labelInterpolationFnc: labelInterpolationFnc,
offset: this.offsetX,
},
axisY: {
labelInterpolationFnc: (value: number): any => this.vbytesPipe.transform(value, 2, 'vB', 'MvB', true),
offset: this.showLegend ? 160 : 60,
},
plugins: this.inverted ? [Chartist.plugins.ctTargetLine({ value: this.stateService.blockVSize })] : []
};
if (this.showLegend) {
const legendNames: string[] = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
250, 300, 350, 400].map((sat, i, arr) => {
if (sat === 400) {
return '350+';
}
if (i === 0) {
return '0 - 1';
}
return arr[i - 1] + ' - ' + sat;
});
// Only Liquid has lower than 1 sat/vb transactions
if (this.stateService.network !== 'liquid') {
legendNames.shift();
}
this.mempoolVsizeFeesOptions.plugins.push(
Chartist.plugins.legend({ legendNames: legendNames })
);
}
this.inverted = this.storageService.getValue('inverted-graph') === 'true';
this.mountFeeChart();
}
ngOnChanges() {
this.inverted = this.storageService.getValue('inverted-graph') === 'true';
this.windowPreference = this.storageService.getValue('graphWindowPreference');
this.mempoolVsizeFeesData = this.handleNewMempoolData(this.data.concat([]));
this.mountFeeChart();
}
onChartReady(myChart: any) {
myChart.getZr().on('mousemove', (e: any) => {
if (e.target !== undefined &&
e.target.parent !== undefined &&
e.target.parent.parent !== null &&
e.target.parent.parent.__ecComponentInfo !== undefined) {
this.hoverIndexSerie = e.target.parent.parent.__ecComponentInfo.index;
}
});
}
handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {
mempoolStats.reverse();
const labels = mempoolStats.map(stats => stats.added);
const finalArrayVbyte = this.generateArray(mempoolStats);
const finalArrayVByte = this.generateArray(mempoolStats);
// Only Liquid has lower than 1 sat/vb transactions
if (this.stateService.network !== 'liquid') {
finalArrayVbyte.shift();
finalArrayVByte.shift();
}
return {
labels: labels,
series: finalArrayVbyte
series: finalArrayVByte
};
}
generateArray(mempoolStats: OptimizedMempoolStats[]) {
const finalArray: number[][] = [];
let feesArray: number[] = [];
for (let index = 37; index > -1; index--) {
const limitFeesTemplate = this.template === 'advanced' ? 28 : 21;
for (let index = limitFeesTemplate; index > -1; index--) {
feesArray = [];
mempoolStats.forEach((stats) => {
const theFee = stats.vsizes[index].toString();
if (theFee) {
feesArray.push(parseInt(theFee, 10));
} else {
feesArray.push(0);
}
feesArray.push(stats.vsizes[index] ? stats.vsizes[index] : 0);
});
if (this.inverted && finalArray.length) {
feesArray = feesArray.map((value, i) => value + finalArray[finalArray.length - 1][i]);
}
finalArray.push(feesArray);
}
finalArray.reverse();
return finalArray;
}
mountFeeChart() {
this.orderLevels();
const { labels, series } = this.mempoolVsizeFeesData;
const seriesGraph = [];
const newColors = [];
for (let index = 0; index < series.length; index++) {
const value = series[index];
if (index >= this.feeLimitIndex) {
newColors.push(this.chartColorsOrdered[index]);
seriesGraph.push({
name: this.feeLevelsOrdered[index],
type: 'line',
stack: 'fees',
smooth: false,
markPoint: {
symbol: 'rect',
},
lineStyle: {
width: 0,
opacity: 0,
},
symbol: 'none',
emphasis: {
focus: 'none',
areaStyle: {
opacity: 0.85,
},
},
markLine: {
silent: true,
symbol: 'none',
lineStyle: {
color: '#fff',
opacity: 1,
width: this.inverted ? 2 : 0,
},
data: [{
yAxis: '1000000',
label: {
show: false,
color: '#ffffff',
}
}],
},
areaStyle: {
color: this.chartColorsOrdered[index],
opacity: 1,
},
data: value
});
}
}
this.mempoolVsizeFeesOptions = {
series: this.inverted ? [...seriesGraph].reverse() : seriesGraph,
hover: true,
color: this.inverted ? [...newColors].reverse() : newColors,
tooltip: {
show: (window.innerWidth >= 768) ? true : false,
trigger: 'axis',
alwaysShowContent: false,
position: (pos, params, el, elRect, size) => {
const positions = { top: (this.template === 'advanced') ? 0 : -30 };
positions[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 60;
return positions;
},
extraCssText: `width: ${(this.template === 'advanced') ? '275px' : '200px'};
background: transparent;
border: none;
box-shadow: none;`,
axisPointer: {
type: 'line',
},
formatter: (params: any) => {
const { totalValue, totalValueArray } = this.getTotalValues(params);
const itemFormatted = [];
let totalParcial = 0;
let progressPercentageText = '';
const items = this.inverted ? [...params].reverse() : params;
items.map((item: any, index: number) => {
totalParcial += item.value;
const progressPercentage = (item.value / totalValue) * 100;
const progressPercentageSum = (totalValueArray[index] / totalValue) * 100;
let activeItemClass = '';
let hoverActive = 0;
if (this.inverted) {
hoverActive = Math.abs(this.feeLevelsOrdered.length - item.seriesIndex - this.feeLevelsOrdered.length);
} else {
hoverActive = item.seriesIndex;
}
if (this.hoverIndexSerie === hoverActive) {
progressPercentageText = `<div class="total-parcial-active">
<span class="progress-percentage">
${formatNumber(progressPercentage, this.locale, '1.2-2')}
<span class="symbol">%</span>
</span>
<span class="total-parcial-vbytes">
${this.vbytesPipe.transform(totalParcial, 2, 'vB', 'MvB', false)}
</span>
<div class="total-percentage-bar">
<span class="total-percentage-bar-background">
<span style="
width: ${progressPercentage}%;
background: ${item.color}
"></span>
</span>
</div>
</div>`;
activeItemClass = 'active';
}
itemFormatted.push(`<tr class="item ${activeItemClass}">
<td class="indicator-container">
<span class="indicator" style="
background-color: ${item.color}
"></span>
<span>
${item.seriesName}
</span>
</td>
<td class="total-progress-sum">
<span>
${this.vbytesPipe.transform(item.value, 2, 'vB', 'MvB', false)}
</span>
</td>
<td class="total-progress-sum">
<span>
${this.vbytesPipe.transform(totalValueArray[index], 2, 'vB', 'MvB', false)}
</span>
</td>
<td class="total-progress-sum-bar">
<span class="total-percentage-bar-background">
<span style="
width: ${progressPercentageSum.toFixed(2)}%;
background-color: ${this.chartColorsOrdered[3]}
"></span>
</span>
</td>
</tr>`);
});
const classActive = (this.template === 'advanced') ? 'fees-wrapper-tooltip-chart-advanced' : '';
return `<div class="fees-wrapper-tooltip-chart ${classActive}">
<div class="title">
${params[0].axisValue}
<span class="total-value">
${this.vbytesPipe.transform(totalValue, 2, 'vB', 'MvB', false)}
</span>
</div>
<table>
<thead>
<tr>
<th>Range</th>
<th>Size</th>
<th>Sum</th>
<th></th>
</tr>
</thead>
<tbody>
${this.inverted ? itemFormatted.join('') : itemFormatted.reverse().join('')}
</tbody>
</table>
<span class="total-value">
${progressPercentageText}
</span>
</div>`;
}
},
dataZoom: [{
type: 'inside',
realtime: true,
zoomOnMouseWheel: (this.template === 'advanced') ? true : false,
maxSpan: 100,
minSpan: 10,
}, {
show: (this.template === 'advanced' && this.showZoom) ? true : false,
type: 'slider',
brushSelect: false,
realtime: true,
bottom: 0,
selectedDataBackground: {
lineStyle: {
color: '#fff',
opacity: 0.45,
},
areaStyle: {
opacity: 0,
}
}
}],
animation: false,
grid: {
height: this.height,
right: this.right,
top: this.top,
left: this.left,
},
xAxis: [
{
type: 'category',
boundaryGap: false,
axisLine: { onZero: true },
axisLabel: {
align: 'center',
fontSize: 11,
lineHeight: 12,
},
data: labels.map((value: any) => `${formatDate(value, 'M/d', this.locale)}\n${formatDate(value, 'H:mm', this.locale)}`),
}
],
yAxis: {
type: 'value',
axisLine: { onZero: false },
axisLabel: {
fontSize: 11,
formatter: (value: number) => (`${this.vbytesPipe.transform(value, 2, 'vB', 'MvB', true)}`),
},
splitLine: {
lineStyle: {
type: 'dotted',
color: '#ffffff66',
opacity: 0.25,
}
}
},
};
}
getTotalValues = (values: any) => {
let totalValueTemp = 0;
const totalValueArray = [];
const valuesInverted = this.inverted ? values : [...values].reverse();
for (const item of valuesInverted) {
totalValueTemp += item.value;
totalValueArray.push(totalValueTemp);
}
return {
totalValue: totalValueTemp,
totalValueArray: totalValueArray.reverse(),
};
}
orderLevels() {
this.feeLevelsOrdered = [];
for (let i = 0; i < feeLevels.length; i++) {
if (feeLevels[i] === this.limitFilterFee) {
this.feeLimitIndex = i;
}
if (feeLevels[i] <= this.limitFee) {
if (i === 0) {
this.feeLevelsOrdered.push('0 - 1');
} else {
this.feeLevelsOrdered.push(`${feeLevels[i - 1]} - ${feeLevels[i]}`);
}
}
}
this.chartColorsOrdered = chartColors.slice(0, this.feeLevelsOrdered.length);
}
}

View File

@@ -23,7 +23,7 @@ export class SearchFormComponent implements OnInit {
searchForm: FormGroup;
@Output() searchTriggered = new EventEmitter();
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[bB]?[a-z]{2,5}1[ac-hj-np-z02-9]{8,87})$/;
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/;
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
regexTransaction = /^[a-fA-F0-9]{64}$/;
regexBlockheight = /^[0-9]+$/;
@@ -107,7 +107,12 @@ export class SearchFormComponent implements OnInit {
this.electrsApiService.getAsset$(searchText)
.subscribe(
() => { this.navigate('/asset/', searchText); },
() => { this.navigate('/tx/', searchText); }
() => {
this.electrsApiService.getBlock$(searchText)
.subscribe(
(block) => { this.navigate('/block/', searchText, { state: { data: { block } } }); },
() => { this.navigate('/tx/', searchText); });
}
);
} else {
this.navigate('/tx/', searchText);
@@ -118,8 +123,8 @@ export class SearchFormComponent implements OnInit {
}
}
navigate(url: string, searchText: string) {
this.router.navigate([(this.network && this.stateService.env.BASE_MODULE === 'mempool' ? '/' + this.network : '') + url, searchText]);
navigate(url: string, searchText: string, extras?: any) {
this.router.navigate([(this.network && this.stateService.env.BASE_MODULE === 'mempool' ? '/' + this.network : '') + url, searchText], extras);
this.searchTriggered.emit();
this.searchForm.setValue({
searchText: '',

View File

@@ -1,11 +0,0 @@
.ct-legend {
top: 130px;
display: flex;
flex-direction: column-reverse;
@media (min-width: 653px) {
top: 90px;
}
}
.ct-legend.inverted {
flex-direction: column !important;
}

View File

@@ -1,740 +0,0 @@
import {
Component,
ElementRef,
Inject,
Input,
OnChanges,
OnDestroy,
OnInit,
PLATFORM_ID,
SimpleChanges,
ViewEncapsulation
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import * as Chartist from '@mempool/chartist';
/**
* Possible chart types
* @type {String}
*/
export type ChartType = 'Pie' | 'Bar' | 'Line';
export type ChartInterfaces =
| Chartist.IChartistPieChart
| Chartist.IChartistBarChart
| Chartist.IChartistLineChart;
export type ChartOptions =
| Chartist.IBarChartOptions
| Chartist.ILineChartOptions
| Chartist.IPieChartOptions;
export type ResponsiveOptionTuple = Chartist.IResponsiveOptionTuple<
ChartOptions
>;
export type ResponsiveOptions = ResponsiveOptionTuple[];
/**
* Represent a chart event.
* For possible values, check the Chartist docs.
*/
export interface ChartEvent {
[eventName: string]: (data: any) => void;
}
@Component({
selector: 'app-chartist',
template: '<ng-content></ng-content>',
styleUrls: ['./chartist.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class ChartistComponent implements OnInit, OnChanges, OnDestroy {
@Input()
// @ts-ignore
public data: Promise<Chartist.IChartistData> | Chartist.IChartistData;
// @ts-ignore
@Input() public type: Promise<ChartType> | ChartType;
@Input()
// @ts-ignore
public options: Promise<Chartist.IChartOptions> | Chartist.IChartOptions;
@Input()
// @ts-ignore
public responsiveOptions: Promise<ResponsiveOptions> | ResponsiveOptions;
// @ts-ignore
@Input() public events: ChartEvent;
isBrowser: boolean = isPlatformBrowser(this.platformId);
// @ts-ignore
public chart: ChartInterfaces;
private element: HTMLElement;
constructor(
element: ElementRef,
@Inject(PLATFORM_ID) private platformId: any,
) {
this.element = element.nativeElement;
}
public ngOnInit(): Promise<ChartInterfaces> {
if (!this.isBrowser) {
return;
}
if (!this.type || !this.data) {
Promise.reject('Expected at least type and data.');
}
return this.renderChart().then((chart) => {
if (this.events !== undefined) {
this.bindEvents(chart);
}
return chart;
});
}
public ngOnChanges(changes: SimpleChanges): void {
if (!this.isBrowser) {
return;
}
this.update(changes);
}
public ngOnDestroy(): void {
if (this.chart) {
this.chart.detach();
}
}
public renderChart(): Promise<ChartInterfaces> {
const promises: any[] = [
this.type,
this.element,
this.data,
this.options,
this.responsiveOptions
];
return Promise.all(promises).then((values) => {
const [type, ...args]: any = values;
if (!(type in Chartist)) {
throw new Error(`${type} is not a valid chart type`);
}
this.chart = (Chartist as any)[type](...args);
return this.chart;
});
}
public update(changes: SimpleChanges): void {
if (!this.chart || 'type' in changes) {
this.renderChart();
} else {
if (changes.data) {
this.data = changes.data.currentValue;
}
if (changes.options) {
this.options = changes.options.currentValue;
}
(this.chart as any).update(this.data, this.options);
}
}
public bindEvents(chart: any): void {
for (const event of Object.keys(this.events)) {
chart.on(event, this.events[event]);
}
}
}
/**
* Chartist.js plugin to display a "target" or "goal" line across the chart.
* Only tested with bar charts. Works for horizontal and vertical bars.
*/
(function(window, document, Chartist) {
'use strict';
const defaultOptions = {
// The class name so you can style the text
className: 'ct-target-line',
// The axis to draw the line. y == vertical bars, x == horizontal
axis: 'y',
// What value the target line should be drawn at
value: null
};
Chartist.plugins = Chartist.plugins || {};
Chartist.plugins.ctTargetLine = function (options: any) {
options = Chartist.extend({}, defaultOptions, options);
return function ctTargetLine (chart: any) {
chart.on('created', function(context: any) {
const projectTarget = {
y: function (chartRect: any, bounds: any, value: any) {
const targetLineY = chartRect.y1 - (chartRect.height() / bounds.max * value);
return {
x1: chartRect.x1,
x2: chartRect.x2,
y1: targetLineY,
y2: targetLineY
};
},
x: function (chartRect: any, bounds: any, value: any) {
const targetLineX = chartRect.x1 + (chartRect.width() / bounds.max * value);
return {
x1: targetLineX,
x2: targetLineX,
y1: chartRect.y1,
y2: chartRect.y2
};
}
};
// @ts-ignore
const targetLine = projectTarget[options.axis](context.chartRect, context.bounds, options.value);
context.svg.elem('line', targetLine, options.className);
});
};
};
}(null, null, Chartist));
/**
* Chartist.js plugin to display a data label on top of the points in a line chart.
*
*/
/* global Chartist */
(function(window, document, Chartist) {
'use strict';
const defaultOptions = {
labelClass: 'ct-label',
labelOffset: {
x: 0,
y: -10
},
textAnchor: 'middle',
align: 'center',
labelInterpolationFnc: Chartist.noop
};
const labelPositionCalculation = {
point: function(data: any) {
return {
x: data.x,
y: data.y
};
},
bar: {
left: function(data: any) {
return {
x: data.x1,
y: data.y1
};
},
center: function(data: any) {
return {
x: data.x1 + (data.x2 - data.x1) / 2,
y: data.y1
};
},
right: function(data: any) {
return {
x: data.x2,
y: data.y1
};
}
}
};
Chartist.plugins = Chartist.plugins || {};
Chartist.plugins.ctPointLabels = function(options: any) {
options = Chartist.extend({}, defaultOptions, options);
function addLabel(position: any, data: any) {
// if x and y exist concat them otherwise output only the existing value
const value = data.value.x !== undefined && data.value.y ?
(data.value.x + ', ' + data.value.y) :
data.value.y || data.value.x;
data.group.elem('text', {
x: position.x + options.labelOffset.x,
y: position.y + options.labelOffset.y,
style: 'text-anchor: ' + options.textAnchor
}, options.labelClass).text(options.labelInterpolationFnc(value));
}
return function ctPointLabels(chart: any) {
if (chart instanceof Chartist.Line || chart instanceof Chartist.Bar) {
chart.on('draw', function(data: any) {
// @ts-ignore
const positonCalculator = labelPositionCalculation[data.type]
// @ts-ignore
&& labelPositionCalculation[data.type][options.align] || labelPositionCalculation[data.type];
if (positonCalculator) {
addLabel(positonCalculator(data), data);
}
});
}
};
};
}(null, null, Chartist));
const defaultOptions = {
className: '',
classNames: false,
removeAll: false,
legendNames: false,
clickable: true,
onClick: null,
position: 'top'
};
Chartist.plugins.legend = function (options: any) {
let cachedDOMPosition;
let cacheInactiveLegends: { [key:number]: boolean } = {};
// Catch invalid options
if (options && options.position) {
if (!(options.position === 'top' || options.position === 'bottom' || options.position instanceof HTMLElement)) {
throw Error('The position you entered is not a valid position');
}
if (options.position instanceof HTMLElement) {
// Detatch DOM element from options object, because Chartist.extend
// currently chokes on circular references present in HTMLElements
cachedDOMPosition = options.position;
delete options.position;
}
}
options = Chartist.extend({}, defaultOptions, options);
if (cachedDOMPosition) {
// Reattatch the DOM Element position if it was removed before
options.position = cachedDOMPosition;
}
return function legend(chart: any) {
var isSelfUpdate = false;
chart.on('created', function (data: any) {
const useLabels = chart instanceof Chartist.Pie && chart.data.labels && chart.data.labels.length;
const legendNames = getLegendNames(useLabels);
var dirtyChartData = (chart.data.series.length < legendNames.length);
if (isSelfUpdate || dirtyChartData)
return;
function removeLegendElement() {
const legendElement = chart.container.querySelector('.ct-legend');
if (legendElement) {
legendElement.parentNode.removeChild(legendElement);
}
}
// Set a unique className for each series so that when a series is removed,
// the other series still have the same color.
function setSeriesClassNames() {
chart.data.series = chart.data.series.map(function (series: any, seriesIndex: any) {
if (typeof series !== 'object') {
series = {
value: series
};
}
series.className = series.className || chart.options.classNames.series + '-' + Chartist.alphaNumerate(seriesIndex);
return series;
});
}
function createLegendElement() {
const legendElement = document.createElement('ul');
legendElement.className = 'ct-legend';
const inverted = localStorage.getItem('inverted-graph') === 'true';
if (inverted){
legendElement.classList.add('inverted');
}
if (chart instanceof Chartist.Pie) {
legendElement.classList.add('ct-legend-inside');
}
if (typeof options.className === 'string' && options.className.length > 0) {
legendElement.classList.add(options.className);
}
if (chart.options.width) {
legendElement.style.cssText = 'width: ' + chart.options.width + 'px;margin: 0 auto;';
}
return legendElement;
}
// Get the right array to use for generating the legend.
function getLegendNames(useLabels: any) {
return options.legendNames || (useLabels ? chart.data.labels : chart.data.series);
}
// Initialize the array that associates series with legends.
// -1 indicates that there is no legend associated with it.
function initSeriesMetadata(useLabels: any) {
const seriesMetadata = new Array(chart.data.series.length);
for (let i = 0; i < chart.data.series.length; i++) {
seriesMetadata[i] = {
data: chart.data.series[i],
label: useLabels ? chart.data.labels[i] : null,
legend: -1
};
}
return seriesMetadata;
}
function createNameElement(i: any, legendText: any, classNamesViable: any) {
const li = document.createElement('li');
li.classList.add('ct-series-' + i);
// Append specific class to a legend element, if viable classes are given
if (classNamesViable) {
li.classList.add(options.classNames[i]);
}
li.setAttribute('data-legend', i);
li.textContent = legendText;
return li;
}
// Append the legend element to the DOM
function appendLegendToDOM(legendElement: any) {
if (!(options.position instanceof HTMLElement)) {
switch (options.position) {
case 'top':
chart.container.insertBefore(legendElement, chart.container.childNodes[0]);
break;
case 'bottom':
chart.container.insertBefore(legendElement, null);
break;
}
} else {
// Appends the legend element as the last child of a given HTMLElement
options.position.insertBefore(legendElement, null);
}
}
function updateChart(newSeries: any, newLabels:any, useLabels: any) {
chart.data.series = newSeries;
if (useLabels) {
chart.data.labels = newLabels;
}
isSelfUpdate = true;
chart.update();
isSelfUpdate = false;
}
function addClickHandler(legendElement: any, legends: any, seriesMetadata: any, useLabels: any) {
legendElement.addEventListener('click', function(e: any) {
const li = e.target;
if (li.parentNode !== legendElement || !li.hasAttribute('data-legend'))
return;
e.preventDefault();
const legendIndex = parseInt(li.getAttribute('data-legend'));
const legend = legends[legendIndex];
const activateLegend = (_legendIndex: number): void => {
legends[_legendIndex].active = true;
legendElement.childNodes[_legendIndex].classList.remove('inactive');
cacheInactiveLegends[_legendIndex] = false;
}
const deactivateLegend = (_legendIndex: number): void => {
legends[_legendIndex].active = false;
legendElement.childNodes[_legendIndex].classList.add('inactive');
cacheInactiveLegends[_legendIndex] = true;
}
for (let i = legends.length - 1; i >= 0; i--) {
if (i >= legendIndex) {
if (!legend.active) activateLegend(i);
} else {
if (legend.active) deactivateLegend(i);
}
}
// Make sure all values are undefined (falsy) when clicking the first legend
// After clicking the first legend all indices should be falsy
if (legendIndex === 0) cacheInactiveLegends = {};
const newSeries = [];
const newLabels = [];
for (let i = 0; i < seriesMetadata.length; i++) {
if (seriesMetadata[i].legend !== -1 && legends[seriesMetadata[i].legend].active) {
newSeries.push(seriesMetadata[i].data);
newLabels.push(seriesMetadata[i].label);
}
}
updateChart(newSeries, newLabels, useLabels);
if (options.onClick) {
options.onClick(chart, e);
}
});
}
removeLegendElement();
const legendElement = createLegendElement();
const seriesMetadata = initSeriesMetadata(useLabels);
const legends: any = [];
// Check if given class names are viable to append to legends
const classNamesViable = Array.isArray(options.classNames) && options.classNames.length === legendNames.length;
var activeSeries = [];
var activeLabels = [];
// Loop through all legends to set each name in a list item.
legendNames.forEach(function (legend: any, i: any) {
const legendText = legend.name || legend;
const legendSeries = legend.series || [i];
const li = createNameElement(i, legendText, classNamesViable);
// If the value is undefined or false, isActive is true
const isActive: boolean = !cacheInactiveLegends[i];
if (isActive) {
activeSeries.push(seriesMetadata[i].data);
activeLabels.push(seriesMetadata[i].label);
} else {
li.classList.add('inactive');
}
legendElement.appendChild(li);
legendSeries.forEach(function(seriesIndex: any) {
seriesMetadata[seriesIndex].legend = i;
});
legends.push({
text: legendText,
series: legendSeries,
active: isActive
});
});
appendLegendToDOM(legendElement);
if (options.clickable) {
setSeriesClassNames();
addClickHandler(legendElement, legends, seriesMetadata, useLabels);
}
updateChart(activeSeries, activeLabels, useLabels);
});
};
};
Chartist.plugins.tooltip = function (options: any) {
options = Chartist.extend({}, defaultOptions, options);
return function tooltip(chart: any) {
let tooltipSelector = options.pointClass;
if (chart instanceof Chartist.Bar) {
tooltipSelector = 'ct-bar';
} else if (chart instanceof Chartist.Pie) {
// Added support for donut graph
if (chart.options.donut) {
tooltipSelector = 'ct-slice-donut';
} else {
tooltipSelector = 'ct-slice-pie';
}
}
const $chart = chart.container;
let $toolTip = $chart.querySelector('.chartist-tooltip');
if (!$toolTip) {
$toolTip = document.createElement('div');
$toolTip.className = (!options.class) ? 'chartist-tooltip' : 'chartist-tooltip ' + options.class;
if (!options.appendToBody) {
$chart.appendChild($toolTip);
} else {
document.body.appendChild($toolTip);
}
}
let height = $toolTip.offsetHeight;
let width = $toolTip.offsetWidth;
hide($toolTip);
function on(event: any, selector: any, callback: any) {
$chart.addEventListener(event, function (e: any) {
if (!selector || hasClass(e.target, selector)) {
callback(e);
}
});
}
on('mouseover', tooltipSelector, function (event: any) {
const $point = event.target;
let tooltipText = '';
const isPieChart = (chart instanceof Chartist.Pie) ? $point : $point.parentNode;
const seriesName = (isPieChart) ? $point.parentNode.getAttribute('ct:meta') || $point.parentNode.getAttribute('ct:series-name') : '';
let meta = $point.getAttribute('ct:meta') || seriesName || '';
const hasMeta = !!meta;
let value = $point.getAttribute('ct:value');
if (options.transformTooltipTextFnc && typeof options.transformTooltipTextFnc === 'function') {
value = options.transformTooltipTextFnc(value, $point.parentNode.getAttribute('class'));
}
if (options.tooltipFnc && typeof options.tooltipFnc === 'function') {
tooltipText = options.tooltipFnc(meta, value);
} else {
if (options.metaIsHTML) {
const txt = document.createElement('textarea');
txt.innerHTML = meta;
meta = txt.value;
}
meta = '<span class="chartist-tooltip-meta">' + meta + '</span>';
if (hasMeta) {
tooltipText += meta + '<br>';
} else {
// For Pie Charts also take the labels into account
// Could add support for more charts here as well!
if (chart instanceof Chartist.Pie) {
const label = next($point, 'ct-label');
if (label) {
tooltipText += text(label) + '<br>';
}
}
}
if (value) {
if (options.currency) {
if (options.currencyFormatCallback != undefined) {
value = options.currencyFormatCallback(value, options);
} else {
value = options.currency + value.replace(/(\d)(?=(\d{3})+(?:\.\d+)?$)/g, '$1,');
}
}
value = '<span class="chartist-tooltip-value">' + value + '</span>';
tooltipText += value;
}
}
if (tooltipText) {
$toolTip.innerHTML = tooltipText;
setPosition(event);
show($toolTip);
// Remember height and width to avoid wrong position in IE
height = $toolTip.offsetHeight;
width = $toolTip.offsetWidth;
}
});
on('mouseout', tooltipSelector, function () {
hide($toolTip);
});
on('mousemove', null, function (event: any) {
if (false === options.anchorToPoint) {
setPosition(event);
}
});
function setPosition(event: any) {
height = height || $toolTip.offsetHeight;
width = width || $toolTip.offsetWidth;
const offsetX = - width / 2 + options.tooltipOffset.x
const offsetY = - height + options.tooltipOffset.y;
let anchorX, anchorY;
if (!options.appendToBody) {
const box = $chart.getBoundingClientRect();
const left = event.pageX - box.left - window.pageXOffset ;
const top = event.pageY - box.top - window.pageYOffset ;
if (true === options.anchorToPoint && event.target.x2 && event.target.y2) {
anchorX = parseInt(event.target.x2.baseVal.value);
anchorY = parseInt(event.target.y2.baseVal.value);
}
$toolTip.style.top = (anchorY || top) + offsetY + 'px';
$toolTip.style.left = (anchorX || left) + offsetX + 'px';
} else {
$toolTip.style.top = event.pageY + offsetY + 'px';
$toolTip.style.left = event.pageX + offsetX + 'px';
}
}
}
};
Chartist.plugins.ctPointLabels = (options) => {
return function ctPointLabels(chart) {
const defaultOptions2 = {
labelClass: 'ct-point-label',
labelOffset: {
x: 0,
y: -7
},
textAnchor: 'middle'
};
options = Chartist.extend({}, defaultOptions2, options);
if (chart instanceof Chartist.Line) {
chart.on('draw', (data) => {
if (data.type === 'point') {
data.group.elem('text', {
x: data.x + options.labelOffset.x,
y: data.y + options.labelOffset.y,
style: 'text-anchor: ' + options.textAnchor
}, options.labelClass).text(options.labelInterpolationFnc(data.value.y)); // 07.11.17 added ".y"
}
});
}
};
};
function show(element: any) {
if (!hasClass(element, 'tooltip-show')) {
element.className = element.className + ' tooltip-show';
}
}
function hide(element: any) {
const regex = new RegExp('tooltip-show' + '\\s*', 'gi');
element.className = element.className.replace(regex, '').trim();
}
function hasClass(element: any, className: any) {
return (' ' + element.getAttribute('class') + ' ').indexOf(' ' + className + ' ') > -1;
}
function next(element: any, className: any) {
do {
element = element.nextSibling;
} while (element && !hasClass(element, className));
return element;
}
function text(element: any) {
return element.innerText || element.textContent;
}

View File

@@ -36,29 +36,75 @@
<input ngbButton type="radio" [value]="'1y'" [routerLink]="['/graphs' | relativeUrl]" fragment="1y"> 1Y
</label>
</div>
<div class="d-inline-block" ngbDropdown #myDrop="ngbDropdown">
<button class="btn btn-primary btn-sm ml-2" id="dropdownFees" ngbDropdownAnchor (click)="myDrop.toggle()">
<fa-icon [icon]="['fas', 'filter']" [fixedWidth]="true" i18n-title="statistics.component-filter.title" title="Filter"></fa-icon>
</button>
<div class="dropdown-fees" ngbDropdownMenu aria-labelledby="dropdownFees">
<ul>
<ng-template ngFor let-fee let-i="index" [ngForOf]="feeLevels">
<ng-template [ngIf]="fee === 1">
<li (click)="filterFees(fee)" [class]="filterFeeIndex > fee ? 'inactive' : ''">
<ng-template [ngIf]="inverted">
<span class="square" [ngStyle]="{'backgroundColor': chartColors[i]}"></span>
</ng-template>
<ng-template [ngIf]="!inverted">
<span class="square" [ngStyle]="{'backgroundColor': chartColors[i - 1]}"></span>
</ng-template>
<span class="fee-text" >0 - {{ fee }}</span>
</li>
</ng-template>
<ng-template [ngIf]="fee <= 500 && fee !== 1">
<li (click)="filterFees(fee)" [class]="filterFeeIndex > fee ? 'inactive' : ''">
<ng-template [ngIf]="inverted">
<span class="square" [ngStyle]="{'backgroundColor': chartColors[i]}"></span>
<span class="fee-text" >{{feeLevels[i - 1]}} - {{ fee }}</span>
</ng-template>
<ng-template [ngIf]="!inverted">
<span class="square" [ngStyle]="{'backgroundColor': chartColors[i - 1]}"></span>
<span class="fee-text" >{{feeLevels[i + 1]}} - {{ fee }}</span>
</ng-template>
</li>
</ng-template>
</ng-template>
</ul>
</div>
</div>
<button (click)="invertGraph()" class="btn btn-primary btn-sm ml-2 d-none d-md-inline"><fa-icon [icon]="['fas', 'exchange-alt']" [rotate]="90" [fixedWidth]="true" i18n-title="statistics.component-invert.title" title="Invert"></fa-icon></button>
</form>
</div>
<div class="card-body">
<div style="height: 600px;">
<app-mempool-graph dir="ltr" [data]="mempoolStats" [dateSpan]="radioGroupForm.controls.dateSpan.value"></app-mempool-graph>
<div class="incoming-transactions-graph">
<app-mempool-graph
dir="ltr"
[template]="'advanced'"
[limitFee]="500"
[limitFilterFee]="filterFeeIndex"
[height]="500"
[left]="65"
[right]="10"
[data]="mempoolStats"
></app-mempool-graph>
</div>
</div>
</div>
</div>
<div class="col-lg-12">
<div>
<div class="card mb-3" *ngIf="mempoolTransactionsWeightPerSecondData">
<div class="card-header">
<i class="fa fa-area-chart"></i> <span i18n="statistics.transaction-vbytes-per-second">Transaction vBytes per second (vB/s)</span>
</div>
<div class="card-body">
<div style="height: 600px;">
<app-chartist
<div class="incoming-transactions-graph">
<app-incoming-transactions-graph
[height]="500"
[left]="65"
[template]="'advanced'"
[data]="mempoolTransactionsWeightPerSecondData"
[type]="'Line'"
[options]="transactionsWeightPerSecondOptions">
</app-chartist>
></app-incoming-transactions-graph>
</div>
</div>
</div>

View File

@@ -56,4 +56,53 @@
text-align: center;
height: 80vh;
justify-content: center;
}
}
.incoming-transactions-graph {
height: 600px;
}
.dropdown-fees {
padding: 10px 0px;
min-width: 130px;
padding: 2px 20px 0px;
left: -38px !important;
position: absolute !important;
ul {
list-style: none;
padding: 0px;
margin-bottom: 0px;
}
li {
width: 100%;
font-size: 14px;
padding: 0px 0px;
padding-left: 20px;
transition: 200ms all ease-in-out;
&:hover {
background-color: #10121e;
cursor: pointer;
}
}
.square {
transition: 200ms all ease-in-out;
height: 12px;
width: 12px;
margin-right: 10px;
border-radius: 1px;
display: inline-block;
position: relative;
top: 1px;
}
.inactive {
.square {
background-color: #ffffff66 !important;
}
.fee-text {
text-decoration: line-through;
color: #777;
}
}
}

View File

@@ -1,6 +1,5 @@
import { Component, OnInit, LOCALE_ID, Inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { formatDate } from '@angular/common';
import { FormGroup, FormBuilder } from '@angular/forms';
import { of, merge} from 'rxjs';
import { switchMap } from 'rxjs/operators';
@@ -9,10 +8,10 @@ import { OptimizedMempoolStats } from '../../interfaces/node-api.interface';
import { WebsocketService } from '../../services/websocket.service';
import { ApiService } from '../../services/api.service';
import * as Chartist from '@mempool/chartist';
import { StateService } from 'src/app/services/state.service';
import { SeoService } from 'src/app/services/seo.service';
import { StorageService } from 'src/app/services/storage.service';
import { feeLevels, chartColors } from 'src/app/app.constants';
@Component({
selector: 'app-statistics',
@@ -24,6 +23,10 @@ export class StatisticsComponent implements OnInit {
loading = true;
spinnerLoading = false;
feeLevels = feeLevels;
chartColors = chartColors;
filterFeeIndex = 1;
dropDownOpen = false;
mempoolStats: OptimizedMempoolStats[] = [];
@@ -31,11 +34,9 @@ export class StatisticsComponent implements OnInit {
mempoolUnconfirmedTransactionsData: any;
mempoolTransactionsWeightPerSecondData: any;
transactionsWeightPerSecondOptions: any;
radioGroupForm: FormGroup;
graphWindowPreference: string;
inverted: boolean;
graphWindowPreference: String;
constructor(
@Inject(LOCALE_ID) private locale: string,
@@ -49,9 +50,13 @@ export class StatisticsComponent implements OnInit {
) { }
ngOnInit() {
this.inverted = this.storageService.getValue('inverted-graph') === 'true';
if (!this.inverted) {
this.feeLevels = [...feeLevels].reverse();
this.chartColors = [...chartColors].reverse();
}
this.seoService.setTitle($localize`:@@5d4f792f048fcaa6df5948575d7cb325c9393383:Graphs`);
this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.inverted = this.storageService.getValue('inverted-graph') === 'true';
this.graphWindowPreference = this.storageService.getValue('graphWindowPreference') ? this.storageService.getValue('graphWindowPreference').trim() : '2h';
const isMobile = window.innerWidth <= 767.98;
let labelHops = isMobile ? 48 : 24;
@@ -64,43 +69,6 @@ export class StatisticsComponent implements OnInit {
dateSpan: this.graphWindowPreference
});
const labelInterpolationFnc = (value: any, index: any) => {
switch (this.graphWindowPreference) {
case '2h':
case '24h':
value = formatDate(value, 'HH:mm', this.locale);
break;
case '1w':
value = formatDate(value, 'dd/MM HH:mm', this.locale);
break;
case '1m':
case '3m':
case '6m':
case '1y':
value = formatDate(value, 'dd/MM', this.locale);
}
return index % labelHops === 0 ? value : null;
};
this.transactionsWeightPerSecondOptions = {
showArea: false,
showLine: true,
showPoint: false,
low: 0,
axisY: {
offset: 40
},
axisX: {
labelInterpolationFnc: labelInterpolationFnc
},
plugins: [
Chartist.plugins.ctTargetLine({
value: 1667
}),
]
};
this.route
.fragment
.subscribe((fragment) => {
@@ -164,12 +132,20 @@ export class StatisticsComponent implements OnInit {
};
}
saveGraphPreference() {
this.storageService.setValue('graphWindowPreference', this.radioGroupForm.controls.dateSpan.value);
}
invertGraph() {
this.storageService.setValue('inverted-graph', !this.inverted);
document.location.reload();
}
saveGraphPreference() {
this.storageService.setValue('graphWindowPreference', this.radioGroupForm.controls.dateSpan.value);
filterFees(index: number) {
this.filterFeeIndex = index;
}
filterClick() {
this.dropDownOpen = !this.dropDownOpen;
}
}

View File

@@ -4,12 +4,18 @@
<div class="spinner-border text-light"></div>
</div>
<div class="tv-container">
<div class="chart-holder" *ngIf="mempoolStats.length">
<app-mempool-graph dir="ltr" [data]="mempoolStats"></app-mempool-graph>
<div class="tv-container" *ngIf="mempoolStats.length">
<div class="chart-holder">
<app-mempool-graph
[template]="'advanced'"
[limitFee]="500"
[height]="600"
[left]="60"
[right]="10"
[data]="mempoolStats"
[showZoom]="false"
></app-mempool-graph>
</div>
<div class="blockchain-wrapper">
<div class="position-container">
<app-mempool-blocks></app-mempool-blocks>

View File

@@ -16,40 +16,24 @@
}
.chart-holder {
height: calc(100vh - 270px);
min-height: 525px;
padding-left: 20px;
width: 98.5%;
padding-top: 20px;
@media(min-width: 992px){
padding-top: 10px;
}
@media(min-height: 800px){
padding-top: 60px !important;
}
height: 650px;
width: 100%;
margin: 30px auto 0;
}
.blockchain-wrapper {
display: flex;
display: block;
height: 100%;
min-height: 240px;
position: relative;
top: -20px;
@media(min-height: 800px) {
top: 10px;
}
top: 30px;
.position-container {
position: absolute;
left: 50%;
bottom: 170px;
}
.chart-holder {
height: calc(100% - 220px);
}
#divider {
width: 3px;
height: 175px;
@@ -64,29 +48,9 @@
top: -28px;
}
}
@media (min-width: 1920px) {
.position-container {
transform: scale(1.3);
bottom: 210px;
}
.chart-holder {
height: calc(100% - 280px);
}
}
}
:host ::ng-deep .ct-legend {
top: 20px !important;
display: flex;
flex-direction: column-reverse;
@media(min-height: 800px){
padding-top: 40px !important;
}
}
.tv-container {
display: flex;
margin-top: 0px;
flex-direction: column;
}
}

View File

@@ -8,13 +8,13 @@
<span class="d-none d-lg-inline">{{ rbfTransaction.txid }}</span>
</a>
</div>
<div>
<div class="title">
<h1 i18n="shared.transaction">Transaction</h1>
</div>
<div class="tx-link">
<div class="tx-link float-left">
<a [routerLink]="['/tx/' | relativeUrl, txId]">
<span class="d-inline d-lg-none">{{ txId | shortenString : 24 }}</span>
<span class="d-none d-lg-inline">{{ txId }}</span>
@@ -41,7 +41,7 @@
<ng-template [ngIf]="!isLoadingTx && !error">
<ng-template [ngIf]="tx.status.confirmed" [ngIfElse]="unconfirmedTemplate">
<div class="box">
<div class="row">
<div class="col-sm">
@@ -50,7 +50,7 @@
<tr>
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
<td>
{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
&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>
</div>
@@ -206,15 +206,15 @@
<tbody>
<tr>
<td i18n="transaction.size|Transaction Size">Size</td>
<td [innerHTML]="tx.size | bytes: 2"></td>
<td [innerHTML]="'&lrm;' + (tx.size | bytes: 2)"></td>
</tr>
<tr>
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
<td [innerHTML]="tx.weight / 4 | vbytes: 2"></td>
<td [innerHTML]="'&lrm;' + (tx.weight / 4 | vbytes: 2)"></td>
</tr>
<tr>
<td i18n="transaction.weight|Transaction Weight">Weight</td>
<td [innerHTML]="tx.weight | wuBytes: 2"></td>
<td [innerHTML]="'&lrm;' + (tx.weight | wuBytes: 2)"></td>
</tr>
<tr>
<td i18n="transaction.hex">Transaction Hex</td>
@@ -309,7 +309,7 @@
<h5 i18n="transaction.error.waiting-for-it-to-appear">Waiting for it to appear in the mempool...</h5>
<div class="spinner-border text-light mt-2"></div>
</div>
<ng-template #errorTemplate>
<div class="text-center">
<h3>{{ error.error }}</h3>

View File

@@ -5,7 +5,7 @@
<span style="float: left;" class="d-none d-md-block">{{ tx.txid }}</span>
</a>
<div class="float-right">
<ng-template [ngIf]="tx.status.confirmed">{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}</ng-template>
<ng-template [ngIf]="tx.status.confirmed">&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}</ng-template>
<ng-template [ngIf]="!tx.status.confirmed && tx.firstSeen">
<i><app-time-since [time]="tx.firstSeen" [fastRender]="true"></app-time-since></i>
</ng-template>

View File

@@ -23,7 +23,7 @@
<div class="col">
<div class="card">
<div class="card-body">
<ng-container *ngTemplateOutlet="txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
<ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
</div>
</div>
</div>
@@ -47,8 +47,13 @@
<ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
<hr>
</div>
<div class="mempool-graph" *ngIf="(mempoolStats$ | async) as mempoolStats">
<app-mempool-graph [data]="mempoolStats.mempool" [showLegend]="false" [offsetX]="20" [small]="true"></app-mempool-graph>
<div class="mempool-graph" *ngIf="(mempoolStats$ | async) as mempoolStats; else loadingSpinner">
<app-mempool-graph
[template]="'widget'"
[limitFee]="150"
[limitFilterFee]="1"
[data]="mempoolStats.mempool"
></app-mempool-graph>
</div>
</div>
</div>
@@ -56,16 +61,19 @@
<div class="col">
<div class="card graph-card">
<div class="card-body">
<ng-container *ngTemplateOutlet="txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
<br>
<ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
<hr>
<div class="mempool-graph" *ngIf="(mempoolStats$ | async) as mempoolStats">
<app-chartist
[data]="mempoolStats.weightPerSecond"
[type]="'Line'"
[options]="transactionsWeightPerSecondOptions">
</app-chartist>
<div class="mempool-graph" *ngIf="stateService.network === 'liquid'; else mempoolGraph">
<app-lbtc-pegs-graph [data]="liquidPegsMonth$ | async"></app-lbtc-pegs-graph>
</div>
<ng-template #mempoolGraph>
<div class="mempool-graph" *ngIf="(mempoolStats$ | async) as mempoolStats; else loadingSpinner">
<app-incoming-transactions-graph
[left]="50"
[data]="mempoolStats.weightPerSecond"
></app-incoming-transactions-graph>
</div>
</ng-template>
</div>
</div>
</div>
@@ -182,13 +190,24 @@
<div class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loadingbig">
<div class="progress">
<div class="progress-bar {{ mempoolInfoData.value.mempoolSizeProgress }}" role="progressbar" [ngStyle]="{'width': (mempoolInfoData.value.memPoolInfo.usage / mempoolInfoData.value.memPoolInfo.maxmempool * 100) + '%' }">&nbsp;</div>
<div class="progress-text"><span [innerHTML]="mempoolInfoData.value.memPoolInfo.usage | bytes"></span> / <span [innerHTML]="mempoolInfoData.value.memPoolInfo.maxmempool | bytes"></span></div>
<div class="progress-text">&lrm;<span [innerHTML]="mempoolInfoData.value.memPoolInfo.usage | bytes"></span> / <span [innerHTML]="mempoolInfoData.value.memPoolInfo.maxmempool | bytes"></span></div>
</div>
</div>
</div>
</div>
</ng-template>
<ng-template #lbtcPegs let-mempoolInfoData>
<div class="mempool-info-data">
<div class="item">
<h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5>
<ng-container *ngIf="(liquidPegsMonth$ | async) as liquidPegsMonth; else loadingTransactions">
<p class="card-text">{{ liquidPegsMonth.series.slice(-1)[0] | number: '1.2-2' }} <span>L-BTC</span></p>
</ng-container>
</div>
</div>
</ng-template>
<ng-template #txPerSecond let-mempoolInfoData>
<h5 class="card-title" i18n="dashboard.incoming-transactions">Incoming transactions</h5>
<ng-template [ngIf]="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value" [ngIfElse]="loadingTransactions">
@@ -197,8 +216,8 @@
</span>
<ng-template #inSync>
<div class="progress inc-tx-progress-bar">
<div class="progress-bar {{ mempoolInfoData.value.progressClass }}" role="progressbar" [ngStyle]="{'width': mempoolInfoData.value.progressWidth}">&nbsp;</div>
<div class="progress-text">{{ mempoolInfoData.value.vBytesPerSecond | ceil | number }} <ng-container i18n="shared.vbytes-per-second|vB/s">vB/s</ng-container></div>
<div class="progress-bar" role="progressbar" [ngStyle]="{'width': mempoolInfoData.value.progressWidth, 'background-color': mempoolInfoData.value.progressColor}">&nbsp;</div>
<div class="progress-text">&lrm;{{ mempoolInfoData.value.vBytesPerSecond | ceil | number }} <ng-container i18n="shared.vbytes-per-second|vB/s">vB/s</ng-container></div>
</div>
</ng-template>
</ng-template>
@@ -228,11 +247,11 @@
<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' }}
{{ epochData.change | absolute | number: '1.2-2' }}
<span class="symbol">%</span>
</div>
<div class="symbol">
<span i18n="difficulty-box.previous">Previous</span>:
<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>
@@ -257,6 +276,12 @@
</ng-template>
<ng-template #loadingSpinner>
<div class="text-center loadingGraphs">
<div class="spinner-border text-light"></div>
</div>
</ng-template>
<ng-template #loadingDifficulty>
<div class="difficulty-skeleton loading-container">
<div class="item">

View File

@@ -58,11 +58,11 @@
display: block;
@media (min-width: 485px) {
display: flex;
flex-direction: row;
flex-direction: row;
}
h5 {
margin-bottom: 10px;
}
}
.item {
width: 50%;
margin: 0px auto 20px;
@@ -131,7 +131,7 @@
.latest-transactions {
width: 100%;
text-align: left;
table-layout:fixed;
table-layout:fixed;
tr, td, th {
border: 0px;
}
@@ -220,6 +220,11 @@
.mempool-graph {
height: 250px;
}
.loadingGraphs{
height: 250px;
display: grid;
place-items: center;
}
.inc-tx-progress-bar {
max-width: 250px;
@@ -247,7 +252,7 @@
color: #ffffff66;
font-size: 12px;
}
.item {
.item {
padding: 0 5px;
width: 100%;
&:nth-child(1) {
@@ -276,25 +281,25 @@
justify-content: space-between;
@media (min-width: 376px) {
flex-direction: row;
}
}
.item {
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;
@@ -355,4 +360,4 @@
.previous-retarget-sign {
margin-right: -2px;
font-size: 10px;
}
}

View File

@@ -2,15 +2,15 @@ import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@
import { combineLatest, merge, Observable, of, timer } from 'rxjs';
import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
import { Block } from '../interfaces/electrs.interface';
import { OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { LiquidPegs, OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface';
import { ApiService } from '../services/api.service';
import { StateService } from '../services/state.service';
import * as Chartist from '@mempool/chartist';
import { formatDate } from '@angular/common';
import { WebsocketService } from '../services/websocket.service';
import { SeoService } from '../services/seo.service';
import { StorageService } from '../services/storage.service';
import { EChartsOption } from 'echarts';
interface MempoolBlocksData {
blocks: number;
@@ -34,7 +34,7 @@ interface MempoolInfoData {
memPoolInfo: MempoolInfo;
vBytesPerSecond: number;
progressWidth: string;
progressClass: string;
progressColor: string;
}
interface MempoolStatsData {
@@ -63,6 +63,7 @@ export class DashboardComponent implements OnInit {
mempoolStats$: Observable<MempoolStatsData>;
transactionsWeightPerSecondOptions: any;
isLoadingWebSocket$: Observable<boolean>;
liquidPegsMonth$: Observable<any>;
constructor(
@Inject(LOCALE_ID) private locale: string,
@@ -74,15 +75,15 @@ export class DashboardComponent implements OnInit {
) { }
ngOnInit(): void {
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.seoService.resetTitle();
this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']);
this.network$ = merge(of(''), this.stateService.networkChanged$);
this.collapseLevel = this.storageService.getValue('dashboard-collapsed') || 'one';
this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$.pipe(
map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100)
);
this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$
.pipe(
map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100)
);
this.mempoolInfoData$ = combineLatest([
this.stateService.mempoolInfo$,
@@ -92,11 +93,21 @@ export class DashboardComponent implements OnInit {
map(([mempoolInfo, vbytesPerSecond]) => {
const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
let progressClass = 'bg-danger';
if (percent <= 75) {
progressClass = 'bg-success';
} else if (percent <= 99) {
progressClass = 'bg-warning';
let progressColor = '#7CB342';
if (vbytesPerSecond > 1667) {
progressColor = '#FDD835';
}
if (vbytesPerSecond > 2000) {
progressColor = '#FFB300';
}
if (vbytesPerSecond > 2500) {
progressColor = '#FB8C00';
}
if (vbytesPerSecond > 3000) {
progressColor = '#F4511E';
}
if (vbytesPerSecond > 3500) {
progressColor = '#D81B60';
}
const mempoolSizePercentage = (mempoolInfo.usage / mempoolInfo.maxmempool * 100);
@@ -111,7 +122,7 @@ export class DashboardComponent implements OnInit {
memPoolInfo: mempoolInfo,
vBytesPerSecond: vbytesPerSecond,
progressWidth: percent + '%',
progressClass: progressClass,
progressColor: progressColor,
mempoolSizeProgress: mempoolSizeProgress,
};
})
@@ -164,7 +175,7 @@ export class DashboardComponent implements OnInit {
}
let colorPreviousAdjustments = '#dc3545';
if (previousRetarget){
if (previousRetarget) {
if (previousRetarget >= 0) {
colorPreviousAdjustments = '#3bcc49';
}
@@ -191,7 +202,6 @@ export class DashboardComponent implements OnInit {
})
);
this.mempoolBlocksData$ = this.stateService.mempoolBlocks$
.pipe(
map((mempoolBlocks) => {
@@ -226,50 +236,48 @@ export class DashboardComponent implements OnInit {
}, []),
);
this.mempoolStats$ = this.stateService.connectionState$.pipe(
filter((state) => state === 2),
switchMap(() => this.apiService.list2HStatistics$()),
switchMap((mempoolStats) => {
return merge(
this.stateService.live2Chart$
.pipe(
scan((acc, stats) => {
acc.unshift(stats);
acc = acc.slice(0, 120);
return acc;
}, mempoolStats)
),
of(mempoolStats)
);
}),
map((mempoolStats) => {
return {
mempool: mempoolStats,
weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
};
}),
share(),
);
this.mempoolStats$ = this.stateService.connectionState$
.pipe(
filter((state) => state === 2),
switchMap(() => this.apiService.list2HStatistics$()),
switchMap((mempoolStats) => {
return merge(
this.stateService.live2Chart$
.pipe(
scan((acc, stats) => {
acc.unshift(stats);
acc = acc.slice(0, 120);
return acc;
}, mempoolStats)
),
of(mempoolStats)
);
}),
map((mempoolStats) => {
const data = this.handleNewMempoolData(mempoolStats.concat([]));
return {
mempool: mempoolStats,
weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
};
}),
share(),
);
this.transactionsWeightPerSecondOptions = {
showArea: false,
showLine: true,
fullWidth: true,
showPoint: false,
low: 0,
axisY: {
offset: 40
},
axisX: {
labelInterpolationFnc: (value: any, index: any) => index % 24 === 0 ? formatDate(value, 'HH:mm', this.locale) : null,
offset: 20
},
plugins: [
Chartist.plugins.ctTargetLine({
value: 1667
if (this.stateService.network === 'liquid') {
this.liquidPegsMonth$ = this.apiService.listLiquidPegsMonth$()
.pipe(
map((pegs) => {
const labels = pegs.map(stats => stats.date);
const series = pegs.map(stats => parseFloat(stats.amount) / 100000000);
series.reduce((prev, curr, i) => series[i] = prev + curr, 0);
return {
series,
labels
};
}),
]
};
share(),
);
}
}
handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {

View File

@@ -6,7 +6,7 @@ export interface OptimizedMempoolStats {
vbytes_per_second: number;
total_fee: number;
mempool_byte_weight: number;
vsizes: number[] | string[];
vsizes: number[];
}
interface Ancestor {
@@ -34,3 +34,21 @@ export interface DifficultyAdjustment {
remainingBlocks: number;
remainingTime: number;
}
export interface AddressInformation {
isvalid: boolean; // (boolean) If the address is valid or not. If not, this is the only property returned.
isvalid_parent?: boolean; // (boolean) Elements only
address: string; // (string) The bitcoin address validated
scriptPubKey: string; // (string) The hex-encoded scriptPubKey generated by the address
isscript: boolean; // (boolean) If the key is a script
iswitness: boolean; // (boolean) If the address is a witness
witness_version?: boolean; // (numeric, optional) The version number of the witness program
witness_program: string; // (string, optional) The hex value of the witness program
confidential_key?: string; // (string) Elements only
unconfidential?: string; // (string) Elements only
}
export interface LiquidPegs {
amount: string;
date: string;
}

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { CpfpInfo, OptimizedMempoolStats, DifficultyAdjustment } from '../interfaces/node-api.interface';
import { CpfpInfo, OptimizedMempoolStats, DifficultyAdjustment, AddressInformation, LiquidPegs } from '../interfaces/node-api.interface';
import { Observable } from 'rxjs';
import { StateService } from './state.service';
import { WebsocketResponse } from '../interfaces/websocket.interface';
@@ -96,4 +96,12 @@ export class ApiService {
getDifficultyAdjustment$(): Observable<DifficultyAdjustment> {
return this.httpClient.get<DifficultyAdjustment>(this.apiBaseUrl + this.apiBasePath + '/api/v1/difficulty-adjustment');
}
validateAddress$(address: string): Observable<AddressInformation> {
return this.httpClient.get<AddressInformation>(this.apiBaseUrl + this.apiBasePath + '/api/v1/validate-address/' + address);
}
listLiquidPegsMonth$(): Observable<LiquidPegs[]> {
return this.httpClient.get<LiquidPegs[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/month');
}
}

View File

@@ -17,7 +17,7 @@ export class HttpCacheInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (this.isBrowser && request.method === 'GET') {
const cachedResponse = this.transferState.get(makeStateKey(request.url), null);
const cachedResponse = this.transferState.get<any>(makeStateKey(request.url), null);
if (cachedResponse) {
const modifiedResponse = new HttpResponse<any>({
headers: cachedResponse.headers,
@@ -35,7 +35,7 @@ export class HttpCacheInterceptor implements HttpInterceptor {
.pipe(tap((event: HttpEvent<any>) => {
if (!this.isBrowser && event instanceof HttpResponse) {
let keyId = request.url.split('/').slice(3).join('/');
this.transferState.set(makeStateKey('/' + keyId), event);
this.transferState.set<any>(makeStateKey('/' + keyId), event);
}
}));
}

View File

@@ -1,15 +1,20 @@
import { Pipe, PipeTransform } from "@angular/core";
import { formatNumber } from "@angular/common";
import { Inject, LOCALE_ID, Pipe, PipeTransform } from "@angular/core";
@Pipe({
name: "feeRounding",
})
export class FeeRoundingPipe implements PipeTransform {
constructor(
@Inject(LOCALE_ID) private locale: string,
) {}
transform(fee: number): string {
if (fee >= 100) {
return fee.toFixed(0);
return formatNumber(fee, this.locale, '1.0-0')
} else if (fee < 10) {
return fee.toFixed(2);
return formatNumber(fee, this.locale, '1.2-2')
}
return fee.toFixed(1);
return formatNumber(fee, this.locale, '1.1-1')
}
}

View File

@@ -14,4 +14,4 @@ export const environment = {
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.

View File

@@ -18,6 +18,7 @@
<meta property="twitter:image:src" content="https://bisq.markets/resources/bisq/bisq-markets-preview.png" />
<meta property="twitter:domain" content="bisq.markets">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-config" content="/resources/bisq/favicons/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
@@ -40,6 +41,7 @@
if (document.location.hostname === "bisq.markets")
{
var _paq = window._paq = window._paq || [];
_paq.push(['disableCookies']);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {

View File

@@ -26,6 +26,7 @@
<link rel="manifest" href="/resources/liquid/favicons/site.webmanifest">
<link id="canonical" rel="canonical" href="https://liquid.network">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-config" content="/resources/liquid/favicons/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
@@ -38,6 +39,7 @@
if (document.location.hostname === "liquid.network")
{
var _paq = window._paq = window._paq || [];
_paq.push(['disableCookies']);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {

View File

@@ -25,6 +25,7 @@
<link rel="shortcut icon" href="/resources/favicons/favicon.ico">
<link id="canonical" rel="canonical" href="https://mempool.space">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-config" content="/resources/favicons/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
@@ -37,6 +38,7 @@
if (document.location.hostname === "mempool.space")
{
var _paq = window._paq = window._paq || [];
_paq.push(['disableCookies']);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {

View File

@@ -2379,6 +2379,7 @@
</trans-unit>
<trans-unit datatype="html" id="8f3ac0544491bf048120928d39e02a6baeb0b278">
<source>Privacy Policy</source>
<target>سیاست حریم‌خصوصی</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/api-docs/api-docs.component.html</context>
<context context-type="linenumber">880,885</context>
@@ -3589,6 +3590,7 @@
</trans-unit>
<trans-unit datatype="html" id="a2fbb7745c7048af923991e08ccd6975364be90d">
<source>Transaction Hex</source>
<target>تراکنش به صورت Hex</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transaction/transaction.component.html</context>
<context context-type="linenumber">220,221</context>

View File

@@ -3270,7 +3270,7 @@
</trans-unit>
<trans-unit datatype="html" id="time-since">
<source><x equiv-text="dateStrings.i18nYear" id="DATE"/> ago</source>
<target><x equiv-text="dateStrings.i18nYear" id="DATE"/> óta</target>
<target><x equiv-text="dateStrings.i18nYear" id="DATE"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time-since/time-since.component.ts</context>
<context context-type="linenumber">67</context>
@@ -3390,7 +3390,7 @@
</trans-unit>
<trans-unit datatype="html" id="time-until">
<source>In ~<x equiv-text="dateStrings.i18nMinute" id="DATE"/></source>
<target>~<x equiv-text="dateStrings.i18nMinute" id="DATE"/> percen belül</target>
<target>~<x equiv-text="dateStrings.i18nMinute" id="DATE"/> belül</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time-until/time-until.component.ts</context>
<context context-type="linenumber">66</context>
@@ -4142,7 +4142,7 @@
</trans-unit>
<trans-unit datatype="html" id="date-base.days">
<source><x equiv-text="counter" id="DATE"/> days</source>
<target><x equiv-text="counter" id="DATE"/> napok</target>
<target><x equiv-text="counter" id="DATE"/> nap</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/i18n/dates.ts</context>
<context context-type="linenumber">10</context>
@@ -4158,7 +4158,7 @@
</trans-unit>
<trans-unit datatype="html" id="date-base.hours">
<source><x equiv-text="counter" id="DATE"/> hours</source>
<target><x equiv-text="counter" id="DATE"/> órák</target>
<target><x equiv-text="counter" id="DATE"/> óra</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/i18n/dates.ts</context>
<context context-type="linenumber">12</context>
@@ -4174,7 +4174,7 @@
</trans-unit>
<trans-unit datatype="html" id="date-base.minutes">
<source><x equiv-text="counter" id="DATE"/> minutes</source>
<target><x equiv-text="counter" id="DATE"/> percek</target>
<target><x equiv-text="counter" id="DATE"/> perce</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/i18n/dates.ts</context>
<context context-type="linenumber">14</context>
@@ -4190,7 +4190,7 @@
</trans-unit>
<trans-unit datatype="html" id="date-base.seconds">
<source><x equiv-text="counter" id="DATE"/> seconds</source>
<target><x equiv-text="counter" id="DATE"/> másodpercek</target>
<target><x equiv-text="counter" id="DATE"/> másodperc</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/i18n/dates.ts</context>
<context context-type="linenumber">16</context>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -99,6 +99,7 @@
</trans-unit>
<trans-unit datatype="html" id="ngb.pagination.last">
<source>»»</source>
<target>»»</target>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/__ivy_ngcc__/fesm2015/@ng-bootstrap/ng-bootstrap/pagination/pagination.ts</context>
<context context-type="linenumber">404</context>

View File

@@ -59,7 +59,7 @@ import '@angular/localize/init';
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
import 'zone.js'; // Included with Angular CLI.
/***************************************************************************************************

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -1,2 +1,2 @@
User-agent: *
Sitemap: https://mempool.space/sitemap.xml
Allow: /

View File

@@ -235,7 +235,7 @@ body {
color: #dc3545;
}
.yellow-color {
.yellow-color {
color: #ffd800;
}
@@ -255,168 +255,308 @@ html:lang(ru) .card-title {
font-size: 0.9rem;
}
/* Chartist */
$ct-series-names: (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z);
$ct-series-colors: (
#D81B60,
#8E24AA,
#5E35B1,
#3949AB,
#1E88E5,
#039BE5,
#00ACC1,
#00897B,
#43A047,
#7CB342,
#C0CA33,
#FDD835,
#FFB300,
#FB8C00,
#F4511E,
#6D4C41,
#757575,
#546E7A,
#b71c1c,
#880E4F,
#4A148C,
#311B92,
#1A237E,
#0D47A1,
#01579B,
#006064,
#004D40,
#1B5E20,
#33691E,
#827717,
#F57F17,
#FF6F00,
#E65100,
#BF360C,
#3E2723,
#212121,
#263238,
#a748ca,
#6188e2,
#a748ca,
#6188e2,
);
/* MEMPOOL CHARTS - start */
@import "../node_modules/@mempool/chartist/dist/scss/chartist.scss";
.ct-bar-label {
font-size: 20px;
font-weight: bold;
fill: #fff;
.mempool-wrapper-tooltip-chart {
height: 250px;
}
.ct-target-line {
stroke: #f5f5f5;
stroke-width: 3px;
stroke-dasharray: 7px;
.echarts {
height: 100%;
min-height: 180px;
}
.ct-area {
stroke: none;
fill-opacity: 0.9;
}
.ct-label {
fill: rgba(255, 255, 255, 0.4);
color: rgba(255, 255, 255, 0.4);
}
.ct-point-label {
fill: rgba(255, 255, 255, 1);
color: rgba(255, 255, 255, 1);
font-size: 14px;
}
.ct-grid {
stroke: rgba(255, 255, 255, 0.2);
}
/* LEGEND */
.ct-legend {
position: absolute;
z-index: 10;
left: 0px;
list-style: none;
font-size: 13px;
padding: 0px 0px 0px 30px;
top: 90px;
li {
position: relative;
padding-left: 23px;
margin-bottom: 0px;
}
li:before {
width: 12px;
height: 12px;
position: absolute;
left: 0;
bottom: 3px;
content: '';
border: 3px solid transparent;
border-radius: 2px;
}
li.inactive:before {
background: transparent;
}
&.ct-legend-inside {
position: absolute;
top: 0;
right: 0;
}
@for $i from 0 to length($ct-series-colors) {
.ct-series-#{$i}:before {
background-color: nth($ct-series-colors, $i + 1);
border-color: nth($ct-series-colors, $i + 1);
.tx-wrapper-tooltip-chart,
.fees-wrapper-tooltip-chart {
background: rgba(#11131f, 0.95);
border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
color: #b1b1b1;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 10px 15px;
text-align: left;
width: 200px;
thead {
th {
font-size: 9px;
color: #b1b1b1;
text-align: right;
&:first-child {
text-align: left;
left: -1px;
position: relative;
}
&:nth-child(4){
display: none;
}
}
}
.title {
font-size: 12px;
font-weight: 700;
margin-bottom: 2px;
color: #fff;
.total-value {
float: right;
}
}
.active {
color: yellow !important;
.value,
.total-partial {
color: yellow !important;
.symbol {
color: yellow !important;
}
}
}
.item {
line-height: 0.8;
.indicator-container {
.indicator {
display: inline-block;
margin-right: 5px;
border-radius: 2px;
margin-top: 5px;
width: 9px;
height: 9px;
}
}
.value {
text-align: right;
.symbol {
color: #7e7e7e;
font-size: 9px !important;
}
}
.symbol {
font-size: 9px;
}
.total-partial {
font-size: 10px;
width: 58px;
text-align: right;
}
.total-percentage-bar {
padding-left: 8px;
}
.total-progress-percentage {
width: 45px;
height: 5px;
text-align: right;
display: none;
}
.total-progress-sum {
width: 58px;
text-align: right;
}
}
.total-label {
width: 100%;
text-align: left;
color: #fff;
margin-top: 5px;
font-size: 14px;
span {
float: right;
}
.symbol {
margin-left: 3px;
font-size: 9px;
position: relative;
top: 2px;
}
}
thead {
th {
font-size: 9px;
color: #b1b1b1;
text-align: right;
&:first-child {
text-align: left;
left: -1px;
position: relative;
}
&:nth-child(4){
display: none;
}
&:nth-child(5){
display: none;
}
}
}
.total-percentage-bar {
margin: auto;
width: 35px;
position: relative;
span {
display: block;
background: #282d47;
height: 5px;
border-radius: 2px;
}
}
.total-parcial-active {
text-align: right;
margin: 5px auto 5px;
padding-left: 0px;
span {
font-size: 10px;
}
.symbol {
font-size: 9px;
}
.total-percentage-bar {
width: 100%;
span {
transition: 1000 all ease-in-out;
}
}
.progress-percentage {
float: left;
}
}
}
.tx-wrapper-tooltip-chart {
width: 115px;
.item {
display: flex;
}
.value {
margin-top: 5px;
}
.indicator-container {
border-radius: 2px;
}
}
.fee-distribution-chart {
height: 250px;
}
.fees-wrapper-tooltip-chart {
.item {
font-size: 9px;
line-height: 0.8;
margin: 0px;
}
.indicator {
margin-right: 5px !important;
border-radius: 1px !important;
margin-top: 0px !important;
}
}
.fees-wrapper-tooltip-chart-advanced,
.tx-wrapper-tooltip-chart-advanced {
background: rgba(#1d1f31, 0.98);
width: 275px;
thead {
th {
&:nth-child(4){
display: table-cell;
}
&:nth-child(5){
display: table-cell;
}
}
}
.title {
font-size: 15px;
margin-bottom: 5px;
}
.item {
line-height: 1.25;
font-size: 11px;
.value {
width: 60px;
.symbol {
font-size: 9px !important;
}
}
.total-partial {
font-size: 10px;
width: 58px;
text-align: right;
}
.total-progress-percentage {
width: 65px;
height: 4px;
padding: 0px 5px;
display: table-cell !important;
border-radius: 4px;
}
.total-progress-sum {
width: 65px;
height: 4px;
padding: 0px 5px;
border-radius: 4px;
}
.total-progress-percentage-bar,
.total-progress-sum-bar {
width: 35px;
height: 4px;
div {
width: 100%;
border-radius: 4px;
display: block;
background: #29324c94;
}
span {
height: 4px;
border-radius: 4px;
display: block;
}
}
}
.total-label {
margin-top: 5px;
font-size: 14px;
span {
float: right;
}
}
.total-parcial-active {
text-align: right;
margin: 5px auto 5px;
span {
font-size: 10px;
}
.total-percentage-bar {
width: 100%;
left: 0;
span {
transition: 1000 all ease-in-out;
}
}
}
.total-percentage-bar-background {
background-color: #282d47;
}
}
.tx-wrapper-tooltip-chart-advanced {
width: 115px;
.indicator-container {
.indicator {
margin-right: 5px;
border-radius: 0px;
margin-top: 5px;
width: 9px;
height: 9px;
}
}
}
.chartist-tooltip {
position: absolute;
display: inline-block;
opacity: 0;
min-width: 5em;
padding: .5em;
background: #F4C63D;
color: #453D3F;
font-family: Oxygen,Helvetica,Arial,sans-serif;
font-weight: 700;
text-align: center;
pointer-events: none;
z-index: 1;
-webkit-transition: opacity .2s linear;
-moz-transition: opacity .2s linear;
-o-transition: opacity .2s linear;
transition: opacity .2s linear; }
.chartist-tooltip:before {
content: "";
position: absolute;
top: 100%;
left: 50%;
width: 0;
height: 0;
margin-left: -15px;
border: 15px solid transparent;
border-top-color: #F4C63D; }
.chartist-tooltip.tooltip-show {
opacity: 1; }
/* MEMPOOL CHARTS - end */
.ct-area, .ct-line {
pointer-events: none; }
.ct-bar {
stroke-width: 1px;
.grow {
flex-grow: 1;
}
hr {
@@ -533,14 +673,19 @@ th {
.reserved { color: #ff8c00 }
.rtl-layout {
.arrow {
@extend .arrow;
.fa-arrow-alt-circle-right {
@extend .fa-arrow-alt-circle-right;
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
}
.table td {
text-align: right;
.fiat {
@extend .fiat;
margin-left: 0px !important;
margin-right: 15px;
}
}
.table th {
@@ -579,6 +724,71 @@ th {
.bitcoin-block {
direction: rtl;
}
.next-previous-blocks {
@extend .next-previous-blocks;
direction: ltr;
}
.tx-link {
@extend .tx-link;
margin-left: 0px;
margin-right: 10px;
}
.pagination-container {
@extend .pagination-container;
ul {
@extend ul;
padding-left: 0px;
padding-right: 5px;
}
}
.search-box-container {
@extend .search-box-container;
margin-right: 0 !important;
margin-left: 0.5rem !important;
}
.code {
@extend .code;
text-align: left !important;
direction: ltr;
.subtitle {
@extend .subtitle;
direction: rtl;
text-align: right !important;
}
}
.container-graph {
@extend .container-graph;
.formRadioGroup {
@extend .formRadioGroup;
direction: ltr;
}
}
.mempool-graph {
@extend .mempool-graph;
direction: ltr;
}
.title-block {
.title {
float: right;
}
}
.container-buttons {
float: left !important;
width: auto !important;
}
.tx-link {
margin-right: 0px;
@media (min-width: 768px) {
margin-right: 10px;
}
}
}
@@ -639,7 +849,7 @@ th {
.card {
background-color: transparent;
padding: 0;
button {
text-align: left;
display: block;
@@ -653,17 +863,17 @@ th {
box-shadow: none;
}
}
.card-header {
padding: 0;
}
.collapsed{
background-color: #2d3348;
color: #1bd8f4;
}
}
.subtitle {
font-weight: bold;
margin-bottom: 3px;
@@ -675,7 +885,6 @@ th {
.pagination-container {
display: inline-block;
width: 100%;
justify-content: space-between;
@@ -698,4 +907,4 @@ th {
.tooltip.show {
width: 220px;
}
}
}

View File

@@ -1,6 +1,6 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,

View File

@@ -84,3 +84,23 @@ ALTER TABLE `transactions`
ALTER TABLE `statistics`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
CREATE TABLE `last_elements_block` (
`block` int(11) NOT NULL,
`datetime` int(11) NOT NULL,
`block_hash` varchar(65) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `last_elements_block` VALUES(0, 0, '');
CREATE TABLE `elements_pegs` (
`block` int(11) NOT NULL,
`datetime` int(11) NOT NULL,
`amount` bigint(20) NOT NULL,
`txid` varchar(65) NOT NULL,
`txindex` int(11) NOT NULL,
`bitcoinaddress` varchar(100) NOT NULL,
`bitcointxid` varchar(65) NOT NULL,
`bitcoinindex` int(11) NOT NULL,
`final_tx` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

View File

@@ -54,10 +54,16 @@ http {
proxy_cache_path /var/cache/nginx keys_zone=cache:20m levels=1:2 inactive=600s max_size=500m;
types_hash_max_size 2048;
# exempt localhost from rate limit
# exempt localhost and local networks from rate limits
geo $limited_ip {
default 1;
127.0.0.1 0;
127.0.0.0/8 0;
10.0.0.0/8 0;
172.16.0.0/12 0;
192.168.0.0/16 0;
169.254.0.0/16 0;
fc00::/7 0;
fe80::/10 0;
}
map $limited_ip $limited_ip_key {
1 $binary_remote_addr;
@@ -89,11 +95,13 @@ http {
~*^he he;
~*^ka ka;
~*^hu hu;
~*^mk mk;
~*^nl nl;
~*^ja ja;
~*^nb nb;
~*^pl pl;
~*^pt pt;
~*^ro ro;
~*^ru ru;
~*^sl sl;
~*^fi fi;
@@ -121,11 +129,13 @@ http {
~*^he he;
~*^ka ka;
~*^hu hu;
~*^mk mk;
~*^nl nl;
~*^ja ja;
~*^nb nb;
~*^pl pl;
~*^pt pt;
~*^ro ro;
~*^ru ru;
~*^sl sl;
~*^fi fi;

View File

@@ -8,6 +8,9 @@
"API_URL_PREFIX": "/api/v1/",
"POLL_RATE_MS": 2000
},
"SYSLOG" : {
"MIN_PRIORITY": "debug"
},
"CORE_RPC": {
"USERNAME": "foo",
"PASSWORD": "bar"

View File

@@ -8,11 +8,19 @@
"API_URL_PREFIX": "/api/v1/",
"WEBSOCKET_REFRESH_RATE_MS": 2000
},
"SYSLOG" : {
"MIN_PRIORITY": "debug"
},
"CORE_RPC": {
"PORT": 7041,
"USERNAME": "foo",
"PASSWORD": "bar"
},
"SECOND_CORE_RPC": {
"PORT": 8332,
"USERNAME": "foo",
"PASSWORD": "bar"
},
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:4001"
},

View File

@@ -7,9 +7,19 @@
"SPAWN_CLUSTER_PROCS": 0,
"API_URL_PREFIX": "/api/v1/",
"CLEAR_PROTECTION_MINUTES": 5,
"POLL_RATE_MS": 2000
"POLL_RATE_MS": 2000,
"USE_SECOND_NODE_FOR_MINFEE": true
},
"SYSLOG" : {
"MIN_PRIORITY": "debug"
},
"CORE_RPC": {
"PORT": 8332,
"USERNAME": "foo",
"PASSWORD": "bar"
},
"SECOND_CORE_RPC": {
"PORT": 8302,
"USERNAME": "foo",
"PASSWORD": "bar"
},

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