Compare commits

...

155 Commits

Author SHA1 Message Date
softsimon
6f8b95a17f Updating i18n. 2021-06-17 11:59:11 -05:00
softsimon
389c1d794c Updated from transifex. 2021-06-17 11:09:52 -05:00
softsimon
fca66f1b9f Updating i18n. 2021-06-17 11:09:07 -05:00
Felipe Knorr Kuhn
4c7d0cd2e5 Generate config on serve and updated git revision method (#587)
* run generate-config on serve

* write the config file only if settings have changed

* read the git commit hash from the current branch, not master

* git sha is now short by default, no need to trim on the about component
2021-06-16 13:47:05 -05:00
Felipe Knorr Kuhn
1016586992 fix the block viewer for liquid (#584) 2021-06-16 11:48:46 -05:00
wiz
38c8f3acb4 More tweaking of project description on About page 2021-06-16 00:19:56 -05:00
wiz
962023fbc4 Update project description on About page 2021-06-15 23:56:49 -05:00
wiz
b4f8bb2f48 Add trademark symbols and trademark notice to About page 2021-06-15 23:23:26 -05:00
Felipe Knorr Kuhn
c26461fada Route json assets to prod (#585)
* update proxy settings to route json assets to prod
2021-06-15 17:32:36 -05:00
softsimon
1a996e1640 Adding missing space. 2021-06-13 17:56:24 -05:00
softsimon
c80532b420 Bumping mempool.js lib. 2021-06-13 15:39:40 -05:00
wiz
74c49b9ae7 Enable i18n locale for Russian (ru) 2021-06-13 15:05:21 -05:00
softsimon
3f03c9c2b6 Swap location of API doc tabs. 2021-06-12 17:18:59 -05:00
softsimon
f00e727e68 Updating i18n from transifex. 2021-06-11 23:27:29 -05:00
Miguel Medeiros
4338dd6c3f Fix transaction title breaks row on some languages (#570) 2021-06-11 23:16:45 -05:00
Felipe Knorr Kuhn
8385c50605 Update test dependencies (#572)
* move Cypress to the optionalDependencies section

* remove protractor

* remove dead protractor test files
2021-06-11 23:06:55 -05:00
softsimon
93c4b1caf1 Fix for missing overpaid fees tag on confirmed txs
fixes #483
2021-06-11 19:30:33 -05:00
softsimon
49810b6a47 Added new i18n strings 2021-06-11 14:54:57 -05:00
softsimon
28d685a661 Updated i18n from transifex 2021-06-11 10:58:54 -05:00
softsimon
95d3d0feaf Bisq transactions was listed in the wrong order.
fixes #566
2021-06-11 10:55:13 -05:00
softsimon
cbc5d67f62 Update README.md 2021-06-10 16:18:26 -05:00
softsimon
87575bc0a2 Add v2.2.0 screen shot 2021-06-10 16:18:09 -05:00
softsimon
8f74ef58f8 i18n fixes. 2021-06-10 15:38:15 -05:00
softsimon
2475c67d5b Use default proxy config for local proxy conf. 2021-06-10 09:55:19 -05:00
softsimon
bf45bf7b39 Update i18n from transifex 2021-06-10 09:52:13 -05:00
Miguel Medeiros
a1f0417997 Sponsor page (#560)
* Refactor sponsors page layout.
Remove add to t-shirts.

* Remove tshirt promo.

* Refactor sponsors page layout.
Remove add to t-shirts.

* Remove bootstrap classes and inline styles.
Add new classes to thanks page.

Co-authored-by: softsimon <softsimon@protonmail.com>
2021-06-09 18:36:34 -05:00
Miguel Medeiros
237f265aab Refactor about page layout. (#559)
* Refactor about page layout.

* Fix loading sponsors layout.
Fix container margins.
Fix sponsor image src.
2021-06-09 18:35:54 -05:00
softsimon
0087700aa5 Adding missing i18n strings on Bisq. 2021-06-09 14:09:25 -05:00
softsimon
861344ed6d Update i18n from transifex. 2021-06-09 13:35:07 -05:00
softsimon
9e343b346a Update i18n. 2021-06-08 22:44:52 -05:00
softsimon
e857dbc874 I18n space fix. 2021-06-08 22:38:48 -05:00
softsimon
a10cd09ba8 I18n space fix. 2021-06-08 22:37:23 -05:00
softsimon
f30777934f Remove tshirt promo. 2021-06-08 21:42:48 -05:00
Miguel Medeiros
4f6bf297bf Fix loading icon position at graph page. (#558) 2021-06-08 00:29:25 -04:00
Miguel Medeiros
0121052f0b Fix lint errors. (#556) 2021-06-07 15:48:27 -04:00
Felipe Knorr Kuhn
1bd0c40c15 New e2e test suite (#555)
* add @cypress/schematic as a dev dependency

* replace protractor with cypress

* remove the boilerplate test

* add the cypress-wait-until helper library

* add cypress-wait-until to all tests

* add signet to stg proxy config

* add proxy config for production

* add environment configs to angular.json

* update e2e target to use the production proxy

* update serve and start scripts to use the new environment config

* add the concurrently lib to the dev dependencies

* fix missing import

* ignore videos and screenshots saved by the e2e suite

* add fixtures for the tests

* update cypress npm scripts to use concurrently

* add some e2e tests
2021-06-07 10:36:41 -04:00
softsimon
2ee96cae44 Remove duplicate i18n string sponsor 2021-06-06 17:16:39 -04:00
softsimon
28c8d7dba0 Updated i18n 2021-06-06 16:56:08 -04:00
softsimon
9b05ecedc6 Address page: Display load more button on load error. (#542)
fixes #440
2021-06-06 16:07:45 -04:00
softsimon
8fbd273733 Empty Bisq blocks was missing. (#541)
fixes #539
2021-06-06 16:07:26 -04:00
Miguel Medeiros
dec8ae2930 Dashboard layout reviewed. (#550)
* Fix mempool-info-data item margin-bottom.

* Remove unecessary bootstrap classes.

* Fix lint errors.

* Fix and remove css classes.

* Add css class to Terms of Service.
2021-06-06 16:07:04 -04:00
Miguel Medeiros
353b0e8729 New API docummentation. (#544)
* Install hljs package.

* Add highlight.js stylesheet and js to index.html.

* Add new instructions for API documentation.

* Add network value to code-template component.

* Add curl examples.

* Fix tab order of bisq api page.

* Add esmodules instalation instructions.
Add external links to repositories.

* Add self-hosted hljs styles.

* Add response code examples.
Add dynamic networks to curl examples.
Remove reponse code box if not needed.

* Self hosted highlight.min.js.

* Bumping "ws" dependency to fix vulnerability.

* npm audit fix

* npm audit fix

* Remove Hightlight.js dependency.
Add new style to code-template-component.

* Remove hljs css.

* Change NgbModule and NgbAccordionModule  to shared

* Fix NgbAccordionModule import.
2021-06-06 16:06:56 -04:00
softsimon
71bfcea8a6 npm audit fix 2021-05-31 19:03:10 -04:00
softsimon
c54c30209e npm audit fix 2021-05-31 19:02:39 -04:00
softsimon
abc6b1519e Bumping "ws" dependency to fix vulnerability. 2021-05-31 18:49:19 -04:00
Miguel Medeiros
4dcda2cf47 Invert graph legends order. (#532)
* Invert graph legends order.

* Reorder graph legends if inverted-graph is true.
2021-05-23 13:51:29 +04:00
Miguel Medeiros
d055fabfeb Fix innerHTML element of status view component. (#537) 2021-05-23 02:16:23 +04:00
Miguel Medeiros
dbb365f5e3 Align loading component in the center. (#535)
* Align loading component in the center.

* Change height of loading component.
2021-05-22 18:57:15 +04:00
Miguel Medeiros
efb5deda43 Fix missing whitespace linting. (#534) 2021-05-21 19:03:18 +04:00
softsimon
a4cd6450e3 Fix for race condition causing cpfp fetching not working. (#533)
fixes #505
2021-05-21 17:06:53 +04:00
softsimon
edad15da0d Upgrading mempool.js 2021-05-21 03:30:39 +04:00
Miguel Medeiros
e70fd0045d Fix tx-list load is being constantly triggered. (#531) 2021-05-20 02:03:59 +04:00
Miguel Medeiros
794bc99cb6 Change chart-holder width. (#530) 2021-05-20 00:44:21 +04:00
Miguel Medeiros
cd1ec53af0 Align scriptmessage text to the left. (#529)
* Align scriptmessage text to the left.
* Script message box only fills the text width.
2021-05-19 01:16:47 +04:00
Miguel Medeiros
3e435d1394 Fix vertical align Graph. (#528)
* Fix graph component vertical css query.

* Fix chart-holder padding-top.
2021-05-19 01:06:51 +04:00
Miguel Medeiros
50b94f8b72 Fix TV View graph height. (#527)
* Fix graph alignment.
Only show the graph on desktop resolutions.

* Fix mempool-info-chart chart alignment.
2021-05-18 23:59:48 +04:00
softsimon
f6f5b69487 Sponsor t-shirts. 2021-05-18 23:00:07 +04:00
wiz
66b27b9dd0 Merge pull request #524 from mempool/simon/sponsor-page
New separate sponsorship page
2021-05-19 00:50:14 +09:00
Miguel Medeiros
71fa2d67cb Fix rbf alert css. (#525) 2021-05-18 18:56:42 +04:00
Miguel Medeiros
5cd2cfa097 FIX unify the units css. (#499)
* FIX unify the units css.
* Fix units css font-size.
2021-05-18 18:20:17 +04:00
softsimon
cfd13b3655 New separate sponsorship page. 2021-05-18 13:23:39 +04:00
wiz
3ffa60db1f Merge pull request #517 from mempool/simon/bisq-address-prefix-search
Handle the 'B' prefix in the search bar autocomplete on /bisq
2021-05-14 03:42:15 +09:00
softsimon
4442964124 Updated regex to accept bisq-addresses. 2021-05-13 21:56:57 +04:00
wiz
cb034020ef Merge pull request #521 from mempool/simon/bisq-price-coloring
Bisq dashboard: Change color between red/green when price changes
2021-05-14 00:53:15 +09:00
softsimon
5aa57d6df9 Bisq dashboard: Change color between red/green when price changes 2021-05-13 19:23:43 +04:00
softsimon
c1a79e3a33 Handle the 'B' prefix in the search bar autocomplete on /bisq
refs #510
2021-05-13 03:01:47 +04:00
wiz
bbd21c9401 Merge pull request #511 from knorrium/improve_scrolling_between_routes
Improve scrolling between routes
2021-05-13 02:03:14 +09:00
wiz
ad22f9cb46 Merge pull request #515 from mempool/simon/round-up-fee-estimates
Round up fee estimations
2021-05-13 01:42:37 +09:00
softsimon
939955fb84 Round up fee estimations 2021-05-12 20:13:55 +04:00
softsimon
63e67dba38 Bisq markets: Add terms of service 2021-05-12 16:07:25 +04:00
softsimon
8a1230623e Adding missing icon import. 2021-05-12 15:57:46 +04:00
softsimon
f20c73af7b Update fiat price every minute instead of every hour. 2021-05-12 15:09:48 +04:00
softsimon
12c99b86b7 Bisq markets: Display terms of service and language selector (except on official markets).
refs #510
2021-05-12 14:51:55 +04:00
softsimon
934dd67384 Updated i18n from transifex. 2021-05-11 15:31:42 +04:00
softsimon
870bd54b38 Updated i18n. 2021-05-11 15:29:03 +04:00
softsimon
89300dae98 Bisq markets: Fix for graph not updating when changing window
refs #510
2021-05-11 14:55:45 +04:00
softsimon
482a891cec Bisq markets: Swapping price locations 2021-05-11 14:37:25 +04:00
softsimon
098ab7d3a7 Bisq market symlinks fix. 2021-05-11 13:25:16 +04:00
softsimon
147d44d14b npm audit fix 2021-05-11 12:43:19 +04:00
Felipe Knorr Kuhn
8ccdf3973c set toggleShowDetails() to scroll into the viewport using anchors 2021-05-10 22:18:30 -07:00
wiz
c09eb651ef Add production/nginx-bisq.conf for bisq.markets usage 2021-05-11 14:18:25 +09:00
Felipe Knorr Kuhn
ac91d814d6 add anchors to the block component 2021-05-10 22:16:42 -07:00
Felipe Knorr Kuhn
be2f024da1 enable scroll position restoration and anchor scrolling 2021-05-10 22:15:54 -07:00
wiz
f137f45cef Merge pull request #508 from mempool/simon/bisq-new-dashboard
New Bisq Markets Dashboard Design
2021-05-11 13:40:32 +09:00
softsimon
90784deacc New Bisq Markets Dashboard Design
fixes #476
2021-05-11 04:15:11 +04:00
softsimon
8ed664e3a9 Update translations from transifex. 2021-05-08 01:12:57 +04:00
softsimon
17b6916f31 Update i18n messages. 2021-05-08 01:11:05 +04:00
Miguel Medeiros
b778d96910 Fix OP_RETURN truncated text. (#502)
* Fix OP_RETURN truncated text.

* Fix scriptmessage width on desktop media queries
2021-05-08 00:36:35 +04:00
wiz
5b2eb16d1c Merge pull request #498 from MiguelMedeiros/op_return
Fix Coinbase and OP_RETURN truncated text.
2021-05-07 00:00:12 +09:00
wiz
af61357ced Merge pull request #478 from mempool/simon/address-page-error-localization
Localize electrum limit error.
2021-05-06 23:04:01 +09:00
wiz
f281e84396 Disable caching of electrs endpoints in production/nginx.conf 2021-05-06 21:20:13 +09:00
Miguel Medeiros
0dc255edf9 Fix OP_RETURN css width. 2021-05-06 09:00:33 -03:00
Miguel Medeiros
2f8f3ca2e9 New concept for dashboard layout components. (#469)
* New concept for dashboard layout components.

* Align dashboard componentes.

* Add divider to fee box component.

* Remove TV icon from mobile and tablet queries.

* Fix form input overflow.

* Add responsive css to statistic component.

* Add responsive css to about page.

* Add global padding bottom.

* Fix graph page styles.

* Add responsive chart and scrollable table.

* Fix mobile css query for navbar menus.

* Fix pagination responsive css.

* Add CSS animated logo.

* Revert "Add CSS animated logo."

This reverts commit 92af38294c0d4fe815a801b37635cde7f8ee1ced.

* Add extra skeleton to fee-box-component.

* Fix latest-blocks and latest-transactions table css.

* Adapt Bisq pages to responsive layout.

* Remove parenthesis from fiat amout.
Fiat prive break not break on desktop.
Transaction ID align left.
Fee box skeleton width resize.

* Fix mobile table text-size.

* Fix dashboard mempool info mobile alignment.
2021-05-03 17:11:30 +04:00
softsimon
39bb93970b Lower height where taproot signaling cal be visible. 2021-05-01 21:46:49 +04:00
softsimon
72d01a0b67 Improve taproot detection. Only display when detected. 2021-05-01 21:03:01 +04:00
softsimon
0b4da88802 Hide taproot signalling until signalling starts. 2021-05-01 04:06:45 +04:00
softsimon
d2fe000ad0 Display block details and taproot signaling. 2021-05-01 03:55:02 +04:00
softsimon
dcedc8a5ff Localize electrum limit error.
fixes #442
2021-04-27 14:21:33 +04:00
wiz
0d03a9e6cc Send HTTP header Vary: Cookie to prevent localization cache bug
Fixes #477
2021-04-27 19:00:33 +09:00
wiz
24b7acdc60 Merge pull request #475 from mempool/simon/difficulty-adjustment-calc-fix
Fixing incorrect difficulty adjustment call
2021-04-27 17:50:43 +09:00
wiz
1000f4dd4d Merge pull request #474 from mempool/simon/bitcoind-tx-push-break-fix
Fix crash issues related to new unconfirmed transactions in bitcoind …
2021-04-27 17:44:19 +09:00
softsimon
d5dba9128e Fixing incorrect difficulty adjustment call
fixes #471
2021-04-27 12:20:11 +04:00
softsimon
84b0375c0c Fix crash issues related to new unconfirmed transactions in bitcoind mode.
fixes #391
2021-04-27 02:13:48 +04:00
softsimon
bf23a6649c Updating from transifex. 2021-04-26 04:05:55 +04:00
softsimon
aea35d4c86 i18n update 2021-04-26 04:03:33 +04:00
softsimon
52b7efdd53 Updated transifex. 2021-04-26 03:45:14 +04:00
wiz
492abad7a6 Fix a few Bisq strings, update transifex source strings 2021-04-26 07:59:15 +09:00
softsimon
f566eae471 Fixed api docs typo. 2021-04-26 02:40:30 +04:00
wiz
2f2be5c64b Capitalize some strings in Bisq components, update i18n 2021-04-26 07:30:34 +09:00
wiz
5d1af0a86e Merge pull request #381 from mempool/simon/bisq-dashboard
Bisq dashboard
2021-04-26 07:17:03 +09:00
softsimon
5cd5280b21 Updated i18n 2021-04-26 01:49:58 +04:00
softsimon
3a957ece05 Merge branch 'master' into simon/bisq-dashboard
# Conflicts:
#	frontend/src/locale/messages.xlf
2021-04-26 01:41:07 +04:00
wiz
3ead05fa51 Add missing i18n tags for various strings, fixes #473 2021-04-26 06:35:56 +09:00
wiz
8a838cd4dc Credit @maciejsoltysiak as translator for Polish 2021-04-26 06:27:51 +09:00
wiz
b05f731332 Update translated strings from Transifex 2021-04-26 06:17:59 +09:00
wiz
06fd821bf8 Update translated strings from Transifex 2021-04-26 05:44:00 +09:00
wiz
6dbfcc9d1a Enable i18n for Polish language 2021-04-26 05:43:47 +09:00
softsimon
001bddd529 Bisq Markets i18n 2021-04-25 22:52:11 +04:00
softsimon
56518b9655 Updated titles 2021-04-25 19:40:43 +04:00
softsimon
da050ee3dc Swap BTC/Fiat columns. 2021-04-25 19:02:26 +04:00
softsimon
5878a2e631 Unified Bisq config 2021-04-25 02:38:46 +04:00
softsimon
c1fc08196b Add whitespaces to missing data points. 2021-04-25 02:07:29 +04:00
softsimon
95a80157a7 Hide latest altcoin trades when not using official bisq markets. 2021-04-24 20:33:56 +04:00
softsimon
165aa6eee2 Don't hide API docs on mobile. 2021-04-23 15:44:01 +04:00
softsimon
b8fe7b621c Merge branch 'master' into simon/bisq-dashboard
# Conflicts:
#	frontend/package-lock.json
#	frontend/src/app/components/master-page/master-page.component.html
2021-04-23 15:35:35 +04:00
softsimon
04ec5e9564 Sort table by number of trades as default sort option, make other columns sortable 2021-04-23 15:09:51 +04:00
softsimon
2d4dff6de8 Display 100 latest trades. 2021-04-23 03:49:55 +04:00
softsimon
5cb98b9813 Make unique URLs for graph timespans 2021-04-21 22:12:35 +04:00
softsimon
d4508bd876 Add loading spinners. 2021-04-21 20:22:34 +04:00
wiz
6ccac1df79 Merge pull request #467 from knorrium/fix_infinite_scrolling
fix infinite scrolling by not using body as the container
2021-04-20 16:47:50 +09:00
Felipe Knorr Kuhn
b38fc824e6 fix infinite scrolling by not using body as the container 2021-04-19 23:48:33 -07:00
softsimon
cdbe90c182 Only enable statistics service from node master process.
fixes #460
2021-04-19 21:39:30 +04:00
softsimon
6b5b80f866 Update chart colors. 2021-04-19 17:39:47 +04:00
Miguel Medeiros
d74677628b New UI for responsive navbar. (#458)
New UI for responsive navbar.
fixes #458
2021-04-19 17:01:04 +04:00
softsimon
f0d46d6ed8 Don't reload after setting graph preference setting.
refs #365
2021-04-19 10:50:24 +04:00
Felipe Knorr Kuhn
220d9afd97 persist graph window preference between reloads (#456)
Persist graph window preference between reloads
fixes #365
2021-04-19 10:17:16 +04:00
wiz
dfd88a7ff9 Merge pull request #454 from knorrium/fix_ios_native_scrolling
fix native scrolling on iOS devices
2021-04-18 19:26:27 +09:00
wiz
eec36ae4e6 Merge pull request #453 from knorrium/bind_server_address 2021-04-18 08:16:56 +09:00
Felipe Knorr Kuhn
0a07a16650 fix native scrolling on iOS devices 2021-04-17 16:11:34 -07:00
Felipe Knorr Kuhn
e62ee72149 bind the server to 0.0.0.0 to allow it to be reachable by other devices 2021-04-17 15:53:22 -07:00
softsimon
117f5410d7 Merge pull request #452 from knorrium/stg_frontend_config
add new configuration and targets to point the frontend to staging
2021-04-18 02:10:09 +04:00
Felipe Knorr Kuhn
f6ea45b61f add new configuration and targets to point the frontend to staging 2021-04-17 14:52:16 -07:00
softsimon
344d1247bd Updated package.json. 2021-04-13 11:30:53 +04:00
softsimon
fcf7955d63 Merge branch 'master' into simon/bisq-dashboard
# Conflicts:
#	frontend/package-lock.json
#	frontend/package.json
2021-04-12 22:22:50 +04:00
softsimon
1ae002385d Merge branch 'simon/cpfp-frontend' into simon/bisq-dashboard
* simon/cpfp-frontend: (46 commits)
  Bugfix: Don't extend already extended transactions to not override the firstSeen property. fixes #390
  Shuffle mempool transactions before saving disk cache. (#398)
  Adding missing return after expressjs response.
  CPFP support (#395)
  Round sat/vB in fee rating tooltip. fixes #364
  Add the GNU AGPLv3 logo to About page
  Update package.json license tags
  Add recommended fee percentile config (#394)
  Fix typo in README (#392)
  Fix icon for Specter Wallet on About page
  Add link to Specter Wallet on our About page
  Add link to WARden Portfolio app as Community Integration on About page
  Delete MIT+CC license from Terms of Service, add AGPLv3 to About page
  Change mempool project license to GNU Affero General Public License v3
  Lower volume for sound effects (#385)
  Improve grammar, layout, and formatting of Terms of Service page
  Display all Project Contributors on About page using GitHub API (#382)
  Modify nginx.conf to cache HTML for 10m and static resources for 1h
  Proxy /api/v1/contributors from mempool.space, also fix HTTP headers
  Add link to Bisq's GitHub repo on About page
  ...
2021-03-21 06:12:41 +07:00
softsimon
dc36bfcfe4 Adding Bisq markets logo. 2021-03-21 02:40:37 +07:00
softsimon
da77dbece1 Bisq markets: General trading volume graph. 2021-03-16 01:17:40 +07:00
softsimon
8e29a4cefd Bisq markets: Titles 2021-03-14 23:24:06 +07:00
softsimon
146fcfc16d Bisq markets: Hide altcoins when not in official bisq markets mode 2021-03-14 02:42:14 +07:00
softsimon
308dd2c7ad Bisq markets: Recent trades. Separate Bisq master page. 2021-03-13 18:24:50 +07:00
softsimon
1d4ed85d50 Bisq markets: Volume and other fixes. 2021-03-10 23:02:55 +07:00
softsimon
d99fd5d59a Bisq markets dashboard: Market backend tracking. WIP. 2021-03-05 15:38:46 +07:00
softsimon
2fca34faaa Bisq markets dashboard: Offers list. WIP. 2021-03-05 02:02:21 +07:00
softsimon
38e866995f Bisq markets dashboard: 24H Volume. WIP. 2021-02-28 17:18:29 +07:00
softsimon
eeb7447988 Bisq markets dashboard. Base views. WIP. 2021-02-27 04:19:56 +07:00
199 changed files with 103808 additions and 82864 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.1.0-dashboard.png)
![mempool](https://mempool.space/resources/screenshots/v2.2.0-dashboard.png)
## Installation Methods

View File

@@ -50,11 +50,7 @@
"ENABLED": true,
"TX_PER_SECOND_SAMPLE_PERIOD": 150
},
"BISQ_BLOCKS": {
"ENABLED": false,
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db/json"
},
"BISQ_MARKETS": {
"BISQ": {
"ENABLED": false,
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
}

View File

@@ -1,12 +1,12 @@
{
"name": "mempool-backend",
"version": "2.0.0",
"version": "2.2.0-dev",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "mempool-backend",
"version": "2.0.0",
"version": "2.2.0-dev",
"license": "GNU Affero General Public License v3.0",
"dependencies": {
"@mempool/bitcoin": "^3.0.2",
@@ -18,13 +18,13 @@
"locutus": "^2.0.12",
"mysql2": "2.2.5",
"node-worker-threads-pool": "^1.4.3",
"ws": "^7.4.4"
"ws": "^7.4.6"
},
"devDependencies": {
"@types/compression": "^1.0.1",
"@types/express": "^4.17.2",
"@types/locutus": "^0.0.6",
"@types/ws": "^6.0.4",
"@types/ws": "^7.4.4",
"tslint": "^6.1.0",
"typescript": "^4.1.5"
}
@@ -163,10 +163,11 @@
}
},
"node_modules/@types/ws": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz",
"integrity": "sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg==",
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-d/7W23JAXPodQNbOZNXvl2K+bqAQrCMwlh/nuQsPSQk6Fq0opHoPrUw43aHsvSbIiQPr8Of2hkFbnz1XBFVyZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
@@ -548,17 +549,17 @@
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"node_modules/elliptic": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"dependencies": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
"hmac-drbg": "^1.0.1",
"inherits": "^2.0.4",
"minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"node_modules/encodeurl": {
@@ -1544,9 +1545,9 @@
"dev": true
},
"node_modules/ws": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
"engines": {
"node": ">=8.3.0"
},
@@ -1698,9 +1699,9 @@
}
},
"@types/ws": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz",
"integrity": "sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg==",
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-d/7W23JAXPodQNbOZNXvl2K+bqAQrCMwlh/nuQsPSQk6Fq0opHoPrUw43aHsvSbIiQPr8Of2hkFbnz1XBFVyZQ==",
"dev": true,
"requires": {
"@types/node": "*"
@@ -2042,17 +2043,17 @@
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"elliptic": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"requires": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
"hmac-drbg": "^1.0.1",
"inherits": "^2.0.4",
"minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"encodeurl": {
@@ -2833,9 +2834,9 @@
"dev": true
},
"ws": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
"requires": {}
},
"yallist": {

View File

@@ -37,13 +37,13 @@
"locutus": "^2.0.12",
"mysql2": "2.2.5",
"node-worker-threads-pool": "^1.4.3",
"ws": "^7.4.4"
"ws": "^7.4.6"
},
"devDependencies": {
"@types/compression": "^1.0.1",
"@types/express": "^4.17.2",
"@types/locutus": "^0.0.6",
"@types/ws": "^6.0.4",
"@types/ws": "^7.4.4",
"tslint": "^6.1.0",
"typescript": "^4.1.5"
}

View File

@@ -8,9 +8,10 @@ import { StaticPool } from 'node-worker-threads-pool';
import logger from '../../logger';
class Bisq {
private static BLOCKS_JSON_FILE_PATH = config.BISQ_BLOCKS.DATA_PATH + '/all/blocks.json';
private static BLOCKS_JSON_FILE_PATH = config.BISQ.DATA_PATH + '/json/all/blocks.json';
private latestBlockHeight = 0;
private blocks: BisqBlock[] = [];
private allBlocks: BisqBlock[] = [];
private transactions: BisqTransaction[] = [];
private transactionIndex: { [txId: string]: BisqTransaction } = {};
private blockIndex: { [hash: string]: BisqBlock } = {};
@@ -98,7 +99,7 @@ class Bisq {
this.topDirectoryWatcher.close();
}
let fsWait: NodeJS.Timeout | null = null;
this.topDirectoryWatcher = fs.watch(config.BISQ_BLOCKS.DATA_PATH, () => {
this.topDirectoryWatcher = fs.watch(config.BISQ.DATA_PATH + '/json', () => {
if (fsWait) {
clearTimeout(fsWait);
}
@@ -126,7 +127,7 @@ class Bisq {
return;
}
let fsWait: NodeJS.Timeout | null = null;
this.subdirectoryWatcher = fs.watch(config.BISQ_BLOCKS.DATA_PATH + '/all', () => {
this.subdirectoryWatcher = fs.watch(config.BISQ.DATA_PATH + '/json/all', () => {
if (fsWait) {
clearTimeout(fsWait);
}
@@ -171,7 +172,7 @@ class Bisq {
this.transactionIndex = {};
this.addressIndex = {};
this.blocks.forEach((block) => {
this.allBlocks.forEach((block) => {
/* Build block index */
if (!this.blockIndex[block.hash]) {
this.blockIndex[block.hash] = block;
@@ -245,9 +246,10 @@ class Bisq {
if (cacheData && cacheData.length !== 0) {
logger.debug('Processing Bisq data dump...');
const data: BisqBlocks = await this.jsonParsePool.exec(cacheData);
if (data.blocks && data.blocks.length !== this.blocks.length) {
this.blocks = data.blocks.filter((block) => block.txs.length > 0);
this.blocks.reverse();
if (data.blocks && data.blocks.length !== this.allBlocks.length) {
this.allBlocks = data.blocks;
this.allBlocks.reverse();
this.blocks = this.allBlocks.filter((block) => block.txs.length > 0);
this.latestBlockHeight = data.chainHeight;
const time = new Date().getTime() - start;
logger.debug('Bisq dump processed in ' + time + ' ms (worker thread)');

View File

@@ -457,6 +457,30 @@ class BisqMarketsApi {
}
}
getVolumesByTime(time: number): MarketVolume[] {
const timestamp_from = new Date().getTime() / 1000 - time;
const timestamp_to = new Date().getTime() / 1000;
const trades = this.getTradesByCriteria(undefined, timestamp_to, timestamp_from,
undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER);
const markets: any = {};
for (const trade of trades) {
if (!markets[trade._market]) {
markets[trade._market] = {
'volume': 0,
'num_trades': 0,
};
}
markets[trade._market]['volume'] += this.fiatCurrenciesIndexed[trade.currency] ? trade._tradeAmount : trade._tradeVolume;
markets[trade._market]['num_trades']++;
}
return markets;
}
private getTradesSummarized(trades: TradesData[], timestamp_from: number, interval?: string): SummarizedIntervals {
const intervals: any = {};
const intervals_prices: any = {};

View File

@@ -6,7 +6,7 @@ import logger from '../../logger';
class Bisq {
private static FOLDER_WATCH_CHANGE_DETECTION_DEBOUNCE = 4000;
private static MARKET_JSON_PATH = config.BISQ_MARKETS.DATA_PATH;
private static MARKET_JSON_PATH = config.BISQ.DATA_PATH;
private static MARKET_JSON_FILE_PATHS = {
activeCryptoCurrency: '/active_crypto_currency_list.json',
activeFiatCurrency: '/active_fiat_currency_list.json',

View File

@@ -147,6 +147,9 @@ class BitcoinApi implements AbstractBitcoinApi {
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, addPrevout);
} else {
esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction);
if (addPrevout) {
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, addPrevout);
}
}
return esploraTransaction;

View File

@@ -43,7 +43,7 @@ class FeeApi {
const multiplier = (pBlock.blockVSize - 500000) / 500000;
return Math.max(Math.round(useFee * multiplier), this.defaultFee);
}
return Math.round(useFee);
return Math.ceil(useFee);
}
}

View File

@@ -16,7 +16,7 @@ class FiatConversion {
public startService() {
logger.info('Starting currency rates service');
setInterval(this.updateCurrency.bind(this), 1000 * 60 * 60);
setInterval(this.updateCurrency.bind(this), 1000 * 60);
this.updateCurrency();
}

View File

@@ -34,7 +34,7 @@ class WebsocketHandler {
this.wss.on('connection', (client: WebSocket) => {
client.on('error', logger.info);
client.on('message', (message: string) => {
client.on('message', async (message: string) => {
try {
const parsedMessage: WebsocketResponse = JSON.parse(message);
const response = {};
@@ -53,7 +53,16 @@ class WebsocketHandler {
if (parsedMessage['watch-mempool']) {
const tx = memPool.getMempool()[client['track-tx']];
if (tx) {
response['tx'] = tx;
if (config.MEMPOOL.BACKEND !== 'esplora') {
try {
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
response['tx'] = fullTx;
} catch (e) {
logger.debug('Error finding transaction in mempool: ' + e.message || e);
}
} else {
response['tx'] = tx;
}
} else {
client['track-mempool-tx'] = parsedMessage['track-tx'];
}
@@ -96,6 +105,14 @@ class WebsocketHandler {
client['track-donation'] = parsedMessage['track-donation'];
}
if (parsedMessage['track-bisq-market']) {
if (/^[a-z]{3}_[a-z]{3}$/.test(parsedMessage['track-bisq-market'])) {
client['track-bisq-market'] = parsedMessage['track-bisq-market'];
} else {
client['track-bisq-market'] = null;
}
}
if (Object.keys(response).length) {
client.send(JSON.stringify(response));
}

View File

@@ -52,11 +52,7 @@ interface IConfig {
ENABLED: boolean;
TX_PER_SECOND_SAMPLE_PERIOD: number;
};
BISQ_BLOCKS: {
ENABLED: boolean;
DATA_PATH: string;
};
BISQ_MARKETS: {
BISQ: {
ENABLED: boolean;
DATA_PATH: string;
};
@@ -114,11 +110,7 @@ const defaults: IConfig = {
'ENABLED': true,
'TX_PER_SECOND_SAMPLE_PERIOD': 150
},
'BISQ_BLOCKS': {
'ENABLED': false,
'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db/json'
},
'BISQ_MARKETS': {
'BISQ': {
'ENABLED': false,
'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db'
},
@@ -133,8 +125,7 @@ class Config implements IConfig {
DATABASE: IConfig['DATABASE'];
SYSLOG: IConfig['SYSLOG'];
STATISTICS: IConfig['STATISTICS'];
BISQ_BLOCKS: IConfig['BISQ_BLOCKS'];
BISQ_MARKETS: IConfig['BISQ_MARKETS'];
BISQ: IConfig['BISQ'];
constructor() {
const configs = this.merge(configFile, defaults);
@@ -146,8 +137,7 @@ class Config implements IConfig {
this.DATABASE = configs.DATABASE;
this.SYSLOG = configs.SYSLOG;
this.STATISTICS = configs.STATISTICS;
this.BISQ_BLOCKS = configs.BISQ_BLOCKS;
this.BISQ_MARKETS = configs.BISQ_MARKETS;
this.BISQ = configs.BISQ;
}
merge = (...objects: object[]): IConfig => {

View File

@@ -81,7 +81,7 @@ class Server {
await checkDbConnection();
}
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isMaster) {
statistics.startStatistics();
}
@@ -90,13 +90,10 @@ class Server {
this.setUpHttpApiRoutes();
this.runMainUpdateLoop();
if (config.BISQ_BLOCKS.ENABLED) {
if (config.BISQ.ENABLED) {
bisq.startBisqService();
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price));
blocks.setNewBlockCallback(bisq.handleNewBitcoinBlock.bind(bisq));
}
if (config.BISQ_MARKETS.ENABLED) {
bisqMarkets.startBisqService();
}
@@ -210,7 +207,7 @@ class Server {
;
}
if (config.BISQ_BLOCKS.ENABLED) {
if (config.BISQ.ENABLED) {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/stats', routes.getBisqStats)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/tx/:txId', routes.getBisqTransaction)
@@ -219,11 +216,6 @@ class Server {
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/blocks/:index/:length', routes.getBisqBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/address/:address', routes.getBisqAddress)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/txs/:index/:length', routes.getBisqTransactions)
;
}
if (config.BISQ_MARKETS.ENABLED) {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/currencies', routes.getBisqMarketCurrencies.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/depth', routes.getBisqMarketDepth.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/hloc', routes.getBisqMarketHloc.bind(routes))
@@ -232,6 +224,7 @@ class Server {
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/ticker', routes.getBisqMarketTicker.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/trades', routes.getBisqMarketTrades.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/volumes', routes.getBisqMarketVolumes.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/volumes/7d', routes.getBisqMarketVolumes7d.bind(routes))
;
}

View File

@@ -73,7 +73,7 @@ class Logger {
}
private getNetwork(): string {
if (config.BISQ_BLOCKS.ENABLED) {
if (config.BISQ.ENABLED) {
return 'bisq';
}
if (config.MEMPOOL.NETWORK && config.MEMPOOL.NETWORK !== 'mainnet') {

View File

@@ -144,6 +144,7 @@ export interface WebsocketResponse {
'track-tx': string;
'track-address': string;
'watch-mempool': boolean;
'track-bisq-market': string;
}
export interface VbytesPerSecond {

View File

@@ -426,6 +426,15 @@ class Routes {
}
}
public getBisqMarketVolumes7d(req: Request, res: Response) {
const result = bisqMarket.getVolumesByTime(604800);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketVolumes7d error'));
}
}
private parseRequestParameters(requestParams: object, params: RequiredSpec): { [name: string]: any; } {
const final = {};
for (const i in params) {

5
frontend/.gitignore vendored
View File

@@ -54,3 +54,8 @@ src/resources/pools.json
# environment config
mempool-frontend-config.json
generated-config.js
# e2e results
cypress/videos
cypress/screenshots

View File

@@ -22,6 +22,7 @@ https://www.transifex.com/mempool/mempool/dashboard/
* Dutch @m__btc
* Japanese @wiz @japananon
* Norwegian @T82771355
* Polish @maciejsoltysiak
* Portugese @jgcastro1985
* Slovenian @thepkbadger
* Finnish @bio_bitcoin
@@ -30,3 +31,4 @@ https://www.transifex.com/mempool/mempool/dashboard/
* Ukrainian @volbil
* Vietnamese @bitcoin_vietnam
* Chinese @wdljt
* Russian @TonyCrusoe @Bitconan

View File

@@ -15,8 +15,8 @@
"prefix": "app",
"i18n": {
"sourceLocale": {
"code":"en-US",
"baseHref":"/"
"code": "en-US",
"baseHref": "/"
},
"locales": {
"ar": {
@@ -71,6 +71,10 @@
"translation": "src/locale/messages.nb.xlf",
"baseHref": "/nb/"
},
"pl": {
"translation": "src/locale/messages.pl.xlf",
"baseHref": "/pl/"
},
"pt": {
"translation": "src/locale/messages.pt.xlf",
"baseHref": "/pt/"
@@ -106,6 +110,10 @@
"zh": {
"translation": "src/locale/messages.zh.xlf",
"baseHref": "/zh/"
},
"ru": {
"translation": "src/locale/messages.ru.xlf",
"baseHref": "/ru/"
}
}
},
@@ -169,6 +177,22 @@
"configurations": {
"production": {
"browserTarget": "mempool:build:production"
},
"local": {
"proxyConfig": "proxy.conf.json",
"verbose": true
},
"staging": {
"proxyConfig": "proxy.stg.conf.json",
"disableHostCheck": true,
"host": "0.0.0.0",
"verbose": true
},
"local-prod": {
"proxyConfig": "proxy.prod.conf.json",
"disableHostCheck": true,
"host": "0.0.0.0",
"verbose": true
}
}
},
@@ -201,8 +225,8 @@
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json",
"tsconfig.server.json"
"tsconfig.server.json",
"cypress/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
@@ -210,10 +234,11 @@
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"builder": "@cypress/schematic:cypress",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "mempool:serve"
"devServerTarget": "mempool:serve:local-prod",
"watch": true,
"headless": false
},
"configurations": {
"production": {
@@ -268,8 +293,27 @@
"configurations": {
"production": {}
}
},
"cypress-run": {
"builder": "@cypress/schematic:cypress",
"options": {
"devServerTarget": "mempool:serve"
},
"configurations": {
"production": {
"devServerTarget": "mempool:serve:production"
}
}
},
"cypress-open": {
"builder": "@cypress/schematic:cypress",
"options": {
"watch": true,
"headless": false
}
}
}
}},
}
},
"defaultProject": "mempool"
}

9
frontend/cypress.json Normal file
View File

@@ -0,0 +1,9 @@
{
"integrationFolder": "cypress/integration",
"supportFile": "cypress/support/index.ts",
"videosFolder": "cypress/videos",
"screenshotsFolder": "cypress/screenshots",
"pluginsFile": "cypress/plugins/index.js",
"fixturesFolder": "cypress/fixtures",
"baseUrl": "http://localhost:4200"
}

View File

@@ -0,0 +1,119 @@
{
"f59c5f3e8141f322276daa63ed5f307085808aea6d4ef9ba61e28154533fdec7": {
"asset_id": "f59c5f3e8141f322276daa63ed5f307085808aea6d4ef9ba61e28154533fdec7",
"contract": {
"entity": {
"domain": "listedreserve.com"
},
"issuer_pubkey": "031cc579d142a03b33cdd745922112821c16e5e8b74e3bd57f16f7fda872b6f1d0",
"name": "Liquid AUD",
"precision": 2,
"ticker": "AUDL",
"version": 0
},
"issuance_txin": {
"txid": "e5c5144ba3dc48259ae29023fe9f7775dec1fc049f456dd3d1f7178e31901fb5",
"vin": 0
},
"issuance_prevout": {
"txid": "ed48be2e035ffa425d2c6faaa82b6a7b648aed1246b6ac76c72e0408db8cf057",
"vout": 1
},
"name": "Liquid AUD",
"ticker": "AUDL",
"precision": 2,
"entity": {
"domain": "listedreserve.com"
},
"version": 0,
"issuer_pubkey": "031cc579d142a03b33cdd745922112821c16e5e8b74e3bd57f16f7fda872b6f1d0"
},
"0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a": {
"asset_id": "0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a",
"contract": {
"entity": {
"domain": "lcad.bullbitcoin.com"
},
"issuer_pubkey": "027fa34026195b05f3aa217335416811dca4f5b579d00271a1bb6304c0152458a8",
"name": "Liquid CAD",
"precision": 8,
"ticker": "LCAD",
"version": 0
},
"issuance_txin": {
"txid": "238badf029cadcf546d90ce23c7eafc2fa2082585c9bd62dc26f1aa11c7bd850",
"vin": 0
},
"issuance_prevout": {
"txid": "a87f13917c08c7ccd8eddb1830c5c9a2bcd59c7d167e9d528659ba40808a6b76",
"vout": 0
},
"name": "Liquid CAD",
"ticker": "LCAD",
"precision": 8,
"entity": {
"domain": "lcad.bullbitcoin.com"
},
"version": 0,
"issuer_pubkey": "027fa34026195b05f3aa217335416811dca4f5b579d00271a1bb6304c0152458a8"
},
"3438ecb49fc45c08e687de4749ed628c511e326460ea4336794e1cf02741329e": {
"asset_id": "3438ecb49fc45c08e687de4749ed628c511e326460ea4336794e1cf02741329e",
"contract": {
"entity": {
"domain": "settlenet.io"
},
"issuer_pubkey": "037b09d542bf7cea6a19fa624b4441790c1a6e44823597bf190e981a846a196541",
"name": "SETTLENET JPY Stablecoin by Crypto Garage",
"precision": 0,
"ticker": "JPYS",
"version": 0
},
"issuance_txin": {
"txid": "e33ad5ce8879297d8bfa7daa193920b94abd3fb12f4e8dade9543dbb292387cb",
"vin": 0
},
"issuance_prevout": {
"txid": "328c4fadd817ea75e634e3648eb4be0bf7e669539b8da921c0f77af3bc148894",
"vout": 1
},
"name": "SETTLENET JPY Stablecoin by Crypto Garage",
"ticker": "JPYS",
"precision": 0,
"entity": {
"domain": "settlenet.io"
},
"version": 0,
"issuer_pubkey": "037b09d542bf7cea6a19fa624b4441790c1a6e44823597bf190e981a846a196541"
},
"ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2": {
"asset_id": "ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2",
"contract": {
"entity": {
"domain": "tether.to"
},
"issuer_pubkey": "0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904",
"name": "Tether USD",
"precision": 8,
"ticker": "USDt",
"version": 0
},
"issuance_txin": {
"txid": "abb4080d91849e933ee2ed65da6b436f7c385cf363fb4aa08399f1e27c58ff3d",
"vin": 0
},
"issuance_prevout": {
"txid": "9596d259270ef5bac0020435e6d859aea633409483ba64e232b8ba04ce288668",
"vout": 0
},
"name": "Tether USD",
"ticker": "USDt",
"precision": 8,
"entity": {
"domain": "tether.to"
},
"version": 0,
"issuer_pubkey": "0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904"
}
}

View File

@@ -0,0 +1,33 @@
{
"f59c5f3e8141f322276daa63ed5f307085808aea6d4ef9ba61e28154533fdec7": [
"listedreserve.com",
"AUDL",
"Liquid AUD",
2
],
"0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a": [
"lcad.bullbitcoin.com",
"LCAD",
"Liquid CAD",
8
],
"6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d": [
null,
"L-BTC",
"Liquid Bitcoin",
8
],
"ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2": [
"tether.to",
"USDt",
"Tether USD",
8
],
"3438ecb49fc45c08e687de4749ed628c511e326460ea4336794e1cf02741329e": [
"settlenet.io",
"JPYS",
"SETTLENET JPY Stablecoin by Crypto Garage",
0
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
describe('Bisq', () => {
beforeEach(() => {
cy.intercept('/sockjs-node/info*').as('socket');
cy.intercept('/bisq/api/markets/hloc?market=btc_usd&interval=day').as('hloc');
cy.intercept('/bisq/api/markets/ticker').as('ticker');
cy.intercept('/bisq/api/markets/markets').as('markets');
cy.intercept('/bisq/api/markets/volumes/7d').as('7d');
cy.intercept('/bisq/api/markets/trades?market=all').as('trades');
cy.intercept('/bisq/api/txs/*/*').as('txs');
cy.intercept('/bisq/api/blocks/*/*').as('blocks');
cy.intercept('/bisq/api/stats').as('stats');
});
it('loads the dashboard', () => {
cy.visit('/bisq');
cy.wait('@socket');
cy.wait('@hloc');
cy.wait('@ticker');
cy.wait('@markets');
cy.wait('@7d');
cy.wait('@trades');
});
it('loads the transactions screen', () => {
cy.visit('/bisq');
cy.get('li:nth-of-type(2) > a').click().then(() => {
cy.wait('@txs');
});
});
it('loads the blocks screen', () => {
cy.visit('/bisq');
cy.get('li:nth-of-type(3) > a').click().then(() => {
cy.wait('@blocks');
});
});
it('loads the stats screen', () => {
cy.visit('/bisq');
cy.get('li:nth-of-type(4) > a').click().then(() => {
cy.wait('@stats');
});
});
it('loads the api screen', () => {
cy.visit('/bisq');
cy.get('li:nth-of-type(5) > a').click().then(() => {
});
});
});

View File

@@ -0,0 +1,81 @@
describe('Liquid', () => {
beforeEach(() => {
// TODO: Fix ng serve to deliver these files
cy.fixture('assets.minimal').then((json) => {
cy.intercept('/resources/assets.minimal.json', json);
});
cy.fixture('assets').then((json) => {
cy.intercept('/resources/assets.json', json);
});
});
it('loads the dashboard', () => {
cy.visit('/liquid');
});
it('loads the blocks page', () => {
cy.visit('/liquid/blocks');
});
it('loads a specific block page', () => {
cy.visit('/liquid/blocks');
});
it('loads the graphs page', () => {
cy.visit('/liquid/graphs');
});
it('loads the tv page - desktop', () => {
cy.visit('/liquid');
cy.get('li:nth-of-type(3) > a').click().then(() => {
cy.wait(1000);
});
});
it('loads the graphs page - mobile', () => {
cy.visit('/liquid');
cy.get('li:nth-of-type(3) > a').click().then(() => {
cy.viewport('iphone-6');
cy.wait(1000);
// TODO: Should we really support TV Mode in Mobile for Bisq?
// cy.get('.tv-only').should('be.visible')
});
});
describe('assets', () => {
it('shows the assets screen', () => {
cy.visit('/liquid');
cy.get('li:nth-of-type(5) > a').click().then(() => {
cy.get('table tr').should('have.length', 5);
});
});
it('allows searching assets', () => {
cy.visit('/liquid');
cy.get('li:nth-of-type(5) > a').click().then(() => {
cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => {
cy.get('table tr').should('have.length', 1);
});
});
});
it('shows a specific asset ID', () => {
cy.visit('/liquid');
cy.get('li:nth-of-type(5) > a').click().then(() => {
cy.get('.container-xl input').click().type('Liquid CAD').then(() => {
cy.get('table tr td:nth-of-type(4) a').click();
});
});
});
it('shows a specific asset issuance TX', () => {
cy.visit('/liquid');
cy.get('li:nth-of-type(5) > a').click().then(() => {
cy.get('.container-xl input').click().type('Liquid CAD').then(() => {
cy.get('table tr td:nth-of-type(5) a').click();
});
});
});
});
});

View File

@@ -0,0 +1,97 @@
describe('Mainnet', () => {
beforeEach(() => {
cy.intercept('/api/block-height/*').as('block-height');
cy.intercept('/api/block/*').as('block');
cy.intercept('/api/block/*/txs/0').as('block-txs');
cy.intercept('/api/tx/*/outspends').as('tx-outspends');
// TODO: Fix ng serve to deliver this file
cy.fixture('pools').then((json) => {
cy.intercept('/resources/pools.json', json);
});
});
it('loads the dashboard', () => {
cy.visit('/');
cy.wait(1000);
});
it('loads the blocks screen', () => {
cy.visit('/');
cy.get('li:nth-of-type(2) > a').click().then(() => {
cy.wait(1000);
});
});
it('loads the graphs screen', () => {
cy.visit('/');
cy.get('li:nth-of-type(3) > a').click().then(() => {
cy.wait(1000);
});
});
describe('tv mode', () => {
it('loads the tv screen - desktop', () => {
cy.viewport('macbook-16');
cy.visit('/');
cy.get('li:nth-of-type(4) > a').click().then(() => {
cy.wait(1000);
cy.get('.tv-only').should('not.be.visible');
});
});
it('loads the tv screen - mobile', () => {
cy.visit('/');
cy.get('li:nth-of-type(4) > a').click().then(() => {
cy.viewport('iphone-6');
cy.wait(1000);
cy.get('.tv-only').should('be.visible');
});
});
});
it('loads the api screen', () => {
cy.visit('/');
cy.get('li:nth-of-type(5) > a').click().then(() => {
cy.wait(1000);
});
});
describe('blocks', () => {
it('shows empty blocks properly', () => {
cy.visit('/block/0000000000000000000bd14f744ef2e006e61c32214670de7eb891a5732ee775');
cy.get('h2').invoke('text').should('equal', '1 transaction');
});
it('expands and collapses the block details', () => {
cy.visit('/block/0');
cy.wait('@tx-outspends');
cy.get('.btn.btn-outline-info').click().then(() => {
cy.get('#details').should('be.visible');
});
cy.get('.btn.btn-outline-info').click().then(() => {
cy.get('#details').should('not.be.visible');
});
});
it('shows blocks with no pagination', () => {
cy.visit('/block/00000000000000000001ba40caf1ad4cec0ceb77692662315c151953bfd7c4c4');
cy.get('h2').invoke('text').should('equal', '19 transactions');
cy.get('ul.pagination').first().children().should('have.length', 5);
});
it('supports pagination on the block screen', () => {
// 41 txs
cy.visit('/block/00000000000000000009f9b7b0f63ad50053ad12ec3b7f5ca951332f134f83d8');
cy.get('.header-bg.box > a').invoke('text').then((text1) => {
cy.get('.active + li').first().click().then(() => {
cy.get('.header-bg.box > a').invoke('text').then((text2) => {
expect(text1).not.to.eq(text2);
});
});
});
});
});
});

View File

@@ -0,0 +1,10 @@
describe('Signet', () => {
it('loads the dashboard', () => {
cy.visit('/signet');
});
it.skip('loads all the pages properly', () => {
});
});

View File

@@ -0,0 +1,9 @@
describe('Testnet', () => {
it('loads the dashboard', () => {
cy.visit('/testnet');
});
it.skip('loads all the pages properly', () => {
});
});

View File

@@ -0,0 +1 @@
module.exports = (on, config) => {}

View File

@@ -0,0 +1,45 @@
// ***********************************************
// This example namespace declaration will help
// with Intellisense and code completion in your
// IDE or Text Editor.
// ***********************************************
// declare namespace Cypress {
// interface Chainable<Subject = any> {
// customCommand(param: any): typeof customCommand;
// }
// }
//
// function customCommand(param: any): void {
// console.warn(param);
// }
//
// NOTE: You can use it like so:
// Cypress.Commands.add('customCommand', customCommand);
//
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
import 'cypress-wait-until';

View File

@@ -0,0 +1,17 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// When a command from ./commands is ready to use, import with `import './commands'` syntax
import './commands';

View File

@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"include": ["**/*.ts"],
"compilerOptions": {
"sourceMap": false,
"types": ["cypress"]
}
}

View File

@@ -1,32 +0,0 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

View File

@@ -1,23 +0,0 @@
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('Welcome to mempool!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});

View File

@@ -1,11 +0,0 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get(browser.baseUrl) as Promise<any>;
}
getTitleText() {
return element(by.css('app-root h1')).getText() as Promise<string>;
}
}

View File

@@ -1,13 +0,0 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es2018",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

View File

@@ -1,4 +1,5 @@
var fs = require('fs');
const { execSync } = require('child_process');
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
const GENERATED_CONFIG_FILE_NAME = 'generated-config.js';
@@ -32,12 +33,13 @@ for (setting in configContent) {
}
try {
gitCommitHash = fs.readFileSync('../.git/refs/heads/master').toString().trim();
const command = 'git rev-parse --short HEAD';
gitCommitHash = execSync(command).toString('utf8').replace(/[\n\r\s]+$/, '');
} catch (e) {
console.log('Could not load git commit info: ' + e.message || e);
}
const code = `(function (window) {
const newConfig = `(function (window) {
window.__env = window.__env || {};${settings.reduce((str, obj) => `${str}
window.__env.${obj.key} = ${ typeof obj.value === 'string' ? `'${obj.value}'` : obj.value };`, '')}
window.__env.GIT_COMMIT_HASH = '${gitCommitHash}';
@@ -45,9 +47,17 @@ const code = `(function (window) {
}(global || this));`;
try {
fs.writeFileSync(GENERATED_CONFIG_FILE_NAME, code, 'utf8');
const currentConfig = fs.readFileSync(GENERATED_CONFIG_FILE_NAME).toString().trim();
if (currentConfig === newConfig) {
console.log("Configuration not changed, skipping generation");
} else {
try {
fs.writeFileSync(GENERATED_CONFIG_FILE_NAME, newConfig, 'utf8');
console.log('Config file generated');
} catch (e) {
throw new Error(e);
}
}
} catch (e) {
throw new Error(e);
}
console.log('Config file generated');

24177
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,8 +24,12 @@
"tsc": "./node_modules/typescript/bin/tsc",
"i18n-extract-from-source": "./node_modules/@angular/cli/bin/ng xi18n --ivy --out-file ./src/locale/messages.xlf",
"i18n-pull-from-transifex": "tx pull -a --parallel --minimum-perc 1 --force",
"serve": "ng serve --proxy-config proxy.conf.json",
"start": "npm run generate-config && npm run sync-assets-dev && ng serve --proxy-config proxy.conf.json",
"serve": "npm run generate-config && ng serve -c local",
"serve:stg": "npm run generate-config && ng serve -c staging",
"serve:local-prod": "npm run generate-config && ng serve -c local-prod",
"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",
"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",
@@ -37,7 +41,9 @@
"dev:ssr": "npm run generate-config && ng run mempool:serve-ssr",
"serve:ssr": "node server.run.js",
"build:ssr": "npm run build && ng run mempool:server:production && ./node_modules/typescript/bin/tsc server.run.ts",
"prerender": "ng run mempool:prerender"
"prerender": "ng run mempool:prerender",
"cypress:open": "concurrently \"ng serve -c local-prod\" \"cypress open\" --kill-others",
"cypress:run": "concurrently \"ng serve -c local-prod\" \"cypress run\" --kill-others"
},
"dependencies": {
"@angular/animations": "~11.2.8",
@@ -55,7 +61,7 @@
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@mempool/chartist": "^0.11.4",
"@mempool/mempool.js": "^2.2.0",
"@mempool/mempool.js": "^2.2.2",
"@ng-bootstrap/ng-bootstrap": "^7.0.0",
"@nguniversal/express-engine": "11.2.1",
"@types/qrcode": "^1.3.4",
@@ -64,6 +70,7 @@
"clipboard": "^2.0.4",
"domino": "^2.1.6",
"express": "^4.17.1",
"lightweight-charts": "^3.3.0",
"ngx-bootrap-multiselect": "^2.0.0",
"ngx-infinite-scroll": "^10.0.1",
"qrcode": "^1.4.4",
@@ -78,12 +85,15 @@
"@angular/cli": "~11.2.7",
"@angular/compiler-cli": "~11.2.8",
"@angular/language-service": "~11.2.8",
"@cypress/schematic": "^1.3.0",
"@nguniversal/builders": "^11.2.1",
"@types/express": "^4.17.0",
"@types/jasmine": "~3.6.0",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^12.11.1",
"codelyzer": "^6.0.1",
"concurrently": "^6.2.0",
"cypress-wait-until": "^1.7.1",
"http-proxy-middleware": "^1.0.5",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
@@ -92,9 +102,11 @@
"karma-coverage": "~2.0.3",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~7.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~4.1.5"
},
"optionalDependencies": {
"cypress": "^7.4.0"
}
}

View File

@@ -88,5 +88,20 @@
"pathRewrite": {
"^/bisq/api": "/api/v1/ws"
}
},
"/resources/assets.minimal.json": {
"target": "https://mempool.space",
"secure": false,
"changeOrigin": true
},
"/resources/assets.json": {
"target": "https://mempool.space",
"secure": false,
"changeOrigin": true
},
"/resources/pools.json": {
"target": "https://mempool.space",
"secure": false,
"changeOrigin": true
}
}
}

View File

@@ -0,0 +1,102 @@
{
"/api/v1/ws": {
"target": "https://mempool.space",
"secure": false,
"ws": true
},
"/api/*": {
"target": "https://mempool.space",
"secure": false,
"changeOrigin": true,
"logLevel": "debug",
"pathRewrite": {
"^/api": "https://mempool.space/api"
},
"timeout": 3600000
},
"/testnet/api/v1/ws": {
"target": "https://mempool.space/testnet",
"secure": false,
"ws": true,
"loglevel": "debug",
"pathRewrite": {
"^/testnet/api": "/api/v1/ws"
}
},
"/testnet/api/*": {
"target": "https://mempool.space",
"secure": true,
"changeOrigin": true,
"loglevel": "debug",
"pathRewrite": {
"/testnet/api": "/testnet/api"
},
"timeout": 3600000
},
"/signet/api/v1/ws": {
"target": "https://mempool.space/signet",
"secure": false,
"ws": true,
"loglevel": "debug",
"pathRewrite": {
"^/signet/api": "/api/v1/ws"
}
},
"/signet/api/*": {
"target": "https://mempool.space",
"secure": true,
"changeOrigin": true,
"loglevel": "debug",
"pathRewrite": {
"/signet/api": "/signet/api"
},
"timeout": 3600000
},
"/bisq/api/v1/ws": {
"target": "https://mempool.space/bisq",
"secure": false,
"ws": true,
"pathRewrite": {
"^/bisq/api": "/api/v1/ws"
}
},
"/bisq/api/*": {
"target": "https://mempool.space/bisq",
"secure": false,
"changeOrigin": true,
"pathRewrite": {
"^/bisq/api/": "/api/v1/bisq/"
},
"timeout": 3600000
},
"/liquid/api/v1/ws": {
"target": "https://mempool.space",
"secure": false,
"ws": true
},
"/liquid/api/*": {
"target": "https://mempool.space",
"secure": false,
"changeOrigin": true,
"pathRewrite": {
"^/liquid/api/": "/liquid/api/"
},
"timeout": 3600000
},
"/resources/assets.minimal.json": {
"target": "https://mempool.space",
"secure": false,
"changeOrigin": true
},
"/resources/assets.json": {
"target": "https://mempool.space",
"secure": false,
"changeOrigin": true
},
"/resources/pools.json": {
"target": "https://mempool.space",
"secure": false,
"changeOrigin": true
}
}

View File

@@ -0,0 +1,100 @@
{
"/api/v1/ws": {
"target": "https://mempool.ninja",
"secure": false,
"ws": true
},
"/api/*": {
"target": "https://mempool.ninja",
"secure": false,
"changeOrigin": true,
"logLevel": "debug",
"pathRewrite": {
"^/api": "https://mempool.ninja/api"
},
"timeout": 3600000
},
"/testnet/api/v1/ws": {
"target": "https://mempool.ninja/testnet",
"secure": false,
"ws": true,
"pathRewrite": {
"^/testnet/api": "/api/v1/ws"
}
},
"/testnet/api/v1/*": {
"target": "https://mempool.ninja/testnet",
"secure": false,
"changeOrigin": true,
"pathRewrite": {
"^/testnet/api/v1": "/api/v1"
},
"timeout": 3600000
},
"/signet/api/v1/ws": {
"target": "https://mempool.ninja/signet",
"secure": false,
"ws": true,
"pathRewrite": {
"^/signet/api": "/api/v1/ws"
}
},
"/signet/api/v1/*": {
"target": "https://mempool.ninja/signet",
"secure": false,
"changeOrigin": true,
"pathRewrite": {
"^/signet/api/v1": "/api/v1"
},
"timeout": 3600000
},
"/bisq/api/v1/ws": {
"target": "https://mempool.ninja/bisq",
"secure": false,
"ws": true,
"pathRewrite": {
"^/bisq/api": "/api/v1/ws"
}
},
"/bisq/api/*": {
"target": "https://mempool.ninja/bisq",
"secure": false,
"changeOrigin": true,
"pathRewrite": {
"^/bisq/api/": "/api/v1/bisq/"
},
"timeout": 3600000
},
"/liquid/api/v1/ws": {
"target": "https://mempool.ninja/liquid",
"secure": false,
"ws": true,
"pathRewrite": {
"^/liquid/api": "/api/v1/ws"
}
},
"/liquid/api/*": {
"target": "https://mempool.ninja/liquid",
"secure": false,
"changeOrigin": true,
"pathRewrite": {
"^/liquid/api/": "/api/v1/liquid/"
},
"timeout": 3600000
},
"/resources/assets.minimal.json": {
"target": "https://mempool.space",
"secure": false,
"changeOrigin": true
},
"/resources/assets.json": {
"target": "https://mempool.space",
"secure": false,
"changeOrigin": true
},
"/resources/pools.json": {
"target": "https://mempool.space",
"secure": false,
"changeOrigin": true
}
}

View File

@@ -16,8 +16,10 @@ import { DashboardComponent } from './dashboard/dashboard.component';
import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
import { ApiDocsComponent } from './components/api-docs/api-docs.component';
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
import { SponsorComponent } from './components/sponsor/sponsor.component';
const routes: Routes = [
let routes: Routes = [
{
path: '',
component: MasterPageComponent,
@@ -69,6 +71,10 @@ const routes: Routes = [
children: [],
component: AddressComponent
},
{
path: 'sponsor',
component: SponsorComponent,
},
],
},
{
@@ -283,9 +289,23 @@ const routes: Routes = [
},
];
const browserWindow = window || {};
// @ts-ignore
const browserWindowEnv = browserWindow.__env || {};
if (browserWindowEnv && browserWindowEnv.OFFICIAL_BISQ_MARKETS) {
routes = [{
path: '',
component: BisqMasterPageComponent,
loadChildren: () => import('./bisq/bisq.module').then(m => m.BisqModule)
}];
}
@NgModule({
imports: [RouterModule.forRoot(routes, {
initialNavigation: 'enabled'
initialNavigation: 'enabled',
scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled'
})],
exports: [RouterModule]
})

View File

@@ -71,11 +71,11 @@ export const languages: Language[] = [
{ code: 'ja', name: '日本語' }, // Japanese
{ code: 'nb', name: 'Norsk' }, // Norwegian Bokmål
// { code: 'nn', name: 'Norsk Nynorsk' }, // Norwegian Nynorsk
// { code: 'pl', name: 'Polski' }, // Polish
{ 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: 'ru', name: 'Русский' }, // Russian
{ code: 'ru', name: 'Русский' }, // Russian
// { code: 'sk', name: 'Slovenčina' }, // Slovak
{ code: 'sl', name: 'Slovenščina' }, // Slovenian
// { code: 'sr', name: 'Српски / srpski' }, // Serbian

View File

@@ -21,6 +21,7 @@ import { WebsocketService } from './services/websocket.service';
import { AddressLabelsComponent } from './components/address-labels/address-labels.component';
import { MempoolBlocksComponent } from './components/mempool-blocks/mempool-blocks.component';
import { MasterPageComponent } from './components/master-page/master-page.component';
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
import { AboutComponent } from './components/about/about.component';
import { TelevisionComponent } from './components/television/television.component';
import { StatisticsComponent } from './components/statistics/statistics.component';
@@ -44,17 +45,20 @@ 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, faBolt, faChartArea, faCogs, faCubes, faDatabase, faExchangeAlt, faInfoCircle,
faLink, faList, faSearch, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faAngleDoubleUp } from '@fortawesome/free-solid-svg-icons';
faLink, faList, faSearch, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faAngleDoubleUp, faChevronDown, faFileAlt, faRedoAlt } from '@fortawesome/free-solid-svg-icons';
import { ApiDocsComponent } from './components/api-docs/api-docs.component';
import { CodeTemplateComponent } from './components/api-docs/code-template.component';
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
import { StorageService } from './services/storage.service';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { SponsorComponent } from './components/sponsor/sponsor.component';
@NgModule({
declarations: [
AppComponent,
AboutComponent,
MasterPageComponent,
BisqMasterPageComponent,
TelevisionComponent,
BlockchainComponent,
StartComponent,
@@ -82,7 +86,9 @@ import { HttpCacheInterceptor } from './services/http-cache.interceptor';
FeesBoxComponent,
DashboardComponent,
ApiDocsComponent,
CodeTemplateComponent,
TermsOfServiceComponent,
SponsorComponent,
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
@@ -127,5 +133,8 @@ export class AppModule {
library.addIcons(faExchangeAlt);
library.addIcons(faAngleDoubleUp);
library.addIcons(faAngleDoubleDown);
library.addIcons(faChevronDown);
library.addIcons(faFileAlt);
library.addIcons(faRedoAlt);
}
}

View File

@@ -1,10 +1,12 @@
<div class="container-xl">
<h1 style="float: left;" i18n="shared.address">Address</h1>
<a [routerLink]="['/address/' | relativeUrl, addressString]" style="line-height: 56px; margin-left: 10px;">
<span class="d-inline d-lg-none">{{ addressString | shortenString : 24 }}</span>
<span class="d-none d-lg-inline">{{ addressString }}</span>
</a>
<app-clipboard [text]="addressString"></app-clipboard>
<h1 i18n="shared.address">Address</h1>
<span class="address-link">
<a [routerLink]="['/address/' | relativeUrl, addressString]">
<span class="d-inline d-lg-none">{{ addressString | shortenString : 24 }}</span>
<span class="d-none d-lg-inline">{{ addressString }}</span>
</a>
<app-clipboard [text]="addressString"></app-clipboard>
</span>
<br>
<div class="clearfix"></div>
@@ -13,26 +15,26 @@
<div class="box">
<div class="row">
<div class="col">
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="address.total-received">Total received</td>
<td>{{ totalReceived / 100 | number: '1.2-2' }} BSQ</td>
<td>{{ totalReceived / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="address.total-sent">Total sent</td>
<td>{{ totalSent / 100 | number: '1.2-2' }} BSQ</td>
<td>{{ totalSent / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="address.balance">Balance</td>
<td>{{ (totalReceived - totalSent) / 100 | number: '1.2-2' }} BSQ (<app-bsq-amount [bsq]="totalReceived - totalSent" [forceFiat]="true" [green]="true"></app-bsq-amount>)</td>
<td>{{ (totalReceived - totalSent) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span> <span class="fiat"><app-bsq-amount [bsq]="totalReceived - totalSent" [forceFiat]="true" [green]="true"></app-bsq-amount></span></td>
</tr>
</tbody>
</table>
</div>
<div class="w-100 d-block d-md-none"></div>
<div class="col qrcode-col">
<div class="col-md qrcode-col">
<div class="qr-wrapper">
<app-qrcode [data]="addressString"></app-qrcode>
</div>

View File

@@ -3,21 +3,73 @@
padding: 10px;
padding-bottom: 5px;
display: inline-block;
margin-right: 25px;
}
@media (min-width: 576px) {
.qrcode-col {
text-align: right;
}
.qrcode-col {
text-align: center;
}
@media (max-width: 575.98px) {
.qrcode-col {
.qrcode-col > div {
margin: 20px auto 5px;
@media (min-width: 768px) {
text-align: center;
margin: auto;
}
}
.qrcode-col > div {
margin-top: 20px;
margin-right: 0px;
.fiat {
display: block;
font-size: 13px;
@media (min-width: 768px) {
display: inline-block;
font-size: 14px;
margin-left: 10px;
}
}
.table {
tr td {
&:last-child {
text-align: right;
@media (min-width: 768px) {
text-align: left;
}
}
}
}
h1 {
margin: 0px;
padding: 0px;
@media (min-width: 576px) {
float: left;
margin-right: 10px;
}
}
.address-link {
line-height: 26px;
margin-left: 0px;
top: 14px;
position: relative;
display: flex;
flex-direction: row;
@media (min-width: 768px) {
line-height: 38px;
}
}
.row{
flex-direction: column;
@media (min-width: 576px) {
flex-direction: row;
}
}
@media (max-width: 767.98px) {
.mobile-bottomcol {
margin-top: 15px;
}
.details-table td:first-child {
white-space: pre-wrap;
}
}

View File

@@ -5,6 +5,7 @@ import { ParamMap, ActivatedRoute } from '@angular/router';
import { Subscription, of } from 'rxjs';
import { BisqTransaction } from '../bisq.interfaces';
import { BisqApiService } from '../bisq-api.service';
import { WebsocketService } from 'src/app/services/websocket.service';
@Component({
selector: 'app-bisq-address',
@@ -22,12 +23,15 @@ export class BisqAddressComponent implements OnInit, OnDestroy {
totalSent = 0;
constructor(
private websocketService: WebsocketService,
private route: ActivatedRoute,
private seoService: SeoService,
private bisqApiService: BisqApiService,
) { }
ngOnInit() {
this.websocketService.want(['blocks']);
this.mainSubscription = this.route.paramMap
.pipe(
switchMap((params: ParamMap) => {

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpResponse, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { BisqTransaction, BisqBlock, BisqStats } from './bisq.interfaces';
import { BisqTransaction, BisqBlock, BisqStats, MarketVolume, Trade, Markets, Tickers, Offers, Currencies, HighLowOpenClose, SummarizedInterval } from './bisq.interfaces';
const API_BASE_URL = '/bisq/api';
@@ -42,4 +42,37 @@ export class BisqApiService {
getAddress$(address: string): Observable<BisqTransaction[]> {
return this.httpClient.get<BisqTransaction[]>(API_BASE_URL + '/address/' + address);
}
getMarkets$(): Observable<Markets> {
return this.httpClient.get<Markets>(API_BASE_URL + '/markets/markets');
}
getMarketsTicker$(): Observable<Tickers> {
return this.httpClient.get<Tickers>(API_BASE_URL + '/markets/ticker');
}
getMarketsCurrencies$(): Observable<Currencies> {
return this.httpClient.get<Currencies>(API_BASE_URL + '/markets/currencies');
}
getMarketsHloc$(market: string, interval: 'minute' | 'half_hour' | 'hour' | 'half_day' | 'day'
| 'week' | 'month' | 'year' | 'auto'): Observable<SummarizedInterval[]> {
return this.httpClient.get<SummarizedInterval[]>(API_BASE_URL + '/markets/hloc?market=' + market + '&interval=' + interval);
}
getMarketOffers$(market: string): Observable<Offers> {
return this.httpClient.get<Offers>(API_BASE_URL + '/markets/offers?market=' + market);
}
getMarketTrades$(market: string): Observable<Trade[]> {
return this.httpClient.get<Trade[]>(API_BASE_URL + '/markets/trades?market=' + market);
}
getMarketVolumesByTime$(period: string): Observable<HighLowOpenClose[]> {
return this.httpClient.get<HighLowOpenClose[]>(API_BASE_URL + '/markets/volumes/' + period);
}
getAllVolumesDay$(): Observable<MarketVolume[]> {
return this.httpClient.get<MarketVolume[]>(API_BASE_URL + '/markets/volumes?interval=week');
}
}

View File

@@ -8,9 +8,9 @@
<ng-template [ngIf]="!isLoading && !error">
<div class="box">
<div class="box block-container">
<div class="row">
<div class="col-sm">
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
@@ -22,13 +22,13 @@
<td>
{{ block.time | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
<i>(<app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since>)</i>
<i class="symbol">(<app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since>)</i>
</div>
</td>
</tr>
</table>
</div>
<div class="col-sm">
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
@@ -73,7 +73,7 @@
<ng-template [ngIf]="isLoading && !error">
<div class="box">
<div class="row">
<div class="col-sm">
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
@@ -86,7 +86,7 @@
</tr>
</table>
</div>
<div class="col-sm">
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>

View File

@@ -1,10 +1,44 @@
.td-width {
width: 175px;
}
@media (max-width: 767.98px) {
.td-width {
width: 140px;
width: 140px;
@media (min-width: 768px) {
width: 175px;
}
}
h1 {
margin: 0px;
padding: 0px;
@media (min-width: 576px) {
float: left;
margin-right: 10px;
}
}
.row{
flex-direction: column;
@media (min-width: 768px) {
flex-direction: row;
}
}
.block-container {
.table {
tr td {
&:last-child {
text-align: right;
@media (min-width: 992px) {
text-align: left;
}
}
}
}
.fiat {
display: block;
font-size: 13px;
@media (min-width: 992px) {
display: inline-block;
font-size: 14px;
margin-left: 10px;
}
}
}

View File

@@ -8,6 +8,7 @@ import { switchMap, catchError } from 'rxjs/operators';
import { SeoService } from 'src/app/services/seo.service';
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
import { HttpErrorResponse } from '@angular/common/http';
import { WebsocketService } from 'src/app/services/websocket.service';
@Component({
selector: 'app-bisq-block',
@@ -23,6 +24,7 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
error: HttpErrorResponse | null;
constructor(
private websocketService: WebsocketService,
private bisqApiService: BisqApiService,
private route: ActivatedRoute,
private seoService: SeoService,
@@ -32,6 +34,8 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
) { }
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.subscription = this.route.paramMap
.pipe(
switchMap((params: ParamMap) => {

View File

@@ -1,5 +1,5 @@
<div class="container-xl">
<h1 style="float: left;" i18n="Bisq blocks header">Blocks</h1>
<h1 style="float: left;" i18n="Bisq blocks header">BSQ Blocks</h1>
<br>
<div class="clearfix"></div>
@@ -18,7 +18,7 @@
<tr *ngFor="let block of blocks.value[0]; trackBy: trackByFn">
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
<td><app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since></td>
<td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }}<span class="d-none d-md-inline"> BSQ</span></td>
<td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
<td class="d-none d-md-block">{{ block.txs.length }}</td>
</tr>
</tbody>
@@ -26,9 +26,9 @@
</div>
<br>
<ngb-pagination *ngIf="blocks.value" [size]="paginationSize" [collectionSize]="blocks.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true"></ngb-pagination>
<div class="pagination">
<ngb-pagination *ngIf="blocks.value" [size]="paginationSize" [collectionSize]="blocks.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true"></ngb-pagination>
</div>
</ng-container>
</div>

View File

@@ -0,0 +1,3 @@
.pagination {
overflow: hidden;
}

View File

@@ -5,6 +5,7 @@ import { Observable } from 'rxjs';
import { BisqBlock, BisqOutput, BisqTransaction } from '../bisq.interfaces';
import { SeoService } from 'src/app/services/seo.service';
import { ActivatedRoute, Router } from '@angular/router';
import { WebsocketService } from 'src/app/services/websocket.service';
@Component({
selector: 'app-bisq-blocks',
@@ -22,9 +23,10 @@ export class BisqBlocksComponent implements OnInit {
isLoading = true;
// @ts-ignore
paginationSize: 'sm' | 'lg' = 'md';
paginationMaxSize = 10;
paginationMaxSize = 4;
constructor(
private websocketService: WebsocketService,
private bisqApiService: BisqApiService,
private seoService: SeoService,
private route: ActivatedRoute,
@@ -32,6 +34,7 @@ export class BisqBlocksComponent implements OnInit {
) { }
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
this.loadingItems = Array(this.itemsPerPage);

View File

@@ -0,0 +1,63 @@
<div class="container-xl">
<h1 i18n="Bisq markets title">Bisq Trading Volume</h1>
<div id="volumeHolder">
<ng-template #loadingVolumes>
<div class="text-center loadingVolumes">
<div class="spinner-border text-light"></div>
</div>
</ng-template>
<ng-container *ngIf="volumes$ | async as volumes; else loadingVolumes">
<app-lightweight-charts-area [data]="volumes.data" [lineData]="volumes.linesData"></app-lightweight-charts-area>
</ng-container>
</div>
<br><br>
<div class="container-info">
<h1>
<ng-template [ngIf]="stateService.env.OFFICIAL_BISQ_MARKETS" [ngIfElse]="nonOfficialMarkets" i18n="Bisq All Markets">Markets</ng-template>
<ng-template #nonOfficialMarkets i18n="Bisq Bitcoin Markets">Bitcoin Markets</ng-template>
</h1>
<ng-container *ngIf="{ value: (tickers$ | async) } as tickers">
<div class="table-container">
<table class="table table-borderless table-striped">
<thead>
<th><ng-container i18n>Currency</ng-container> <button [disabled]="(sort$ | async) === 'name'" class="btn btn-link btn-sm" (click)="sort('name')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
<th i18n>Price</th>
<th><ng-container i18n="Trading volume 7D">Volume (7d)</ng-container> <button [disabled]="(sort$ | async) === 'volumes'" class="btn btn-link btn-sm" (click)="sort('volumes')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
<th><ng-container i18n="Trades amount 7D">Trades (7d)</ng-container> <button [disabled]="(sort$ | async) === 'trades'" class="btn btn-link btn-sm" (click)="sort('trades')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
</thead>
<tbody *ngIf="tickers.value; else loadingTmpl">
<tr *ngFor="let ticker of tickers.value; trackBy: trackByFn;">
<td><a [routerLink]="['/market' | relativeUrl, ticker.pair_url]">{{ ticker.name }})</a></td>
<td>
<app-fiat *ngIf="ticker.market.rtype === 'crypto'; else fiat" [value]="ticker.last * 100000000"></app-fiat>
<ng-template #fiat>
<span class="green-color">{{ ticker.last | currency: ticker.market.rsymbol }}</span>
</ng-template>
</td>
<td>
<app-fiat [value]="ticker.volume?.volume"></app-fiat>
</td>
<td>{{ ticker.volume?.num_trades ? ticker.volume?.num_trades : 0 }}</td>
</tr>
</tbody>
</table>
</div>
<br><br>
<h2 i18n="Latest Trades header">Latest Trades</h2>
<app-bisq-trades [trades$]="trades$"></app-bisq-trades>
</ng-container>
</div>
</div>
<ng-template #loadingTmpl>
<tr *ngFor="let i of [1,2,3,4,5,6,7,8,9,10]">
<td *ngFor="let j of [1, 2, 3, 4]"><span class="skeleton-loader"></span></td>
</tr>
</ng-template>

View File

@@ -0,0 +1,35 @@
#volumeHolder {
height: 500px;
background-color: #000;
overflow: hidden;
display: flex;
justify-content: center;
}
.table {
max-width: 100%;
overflow: scroll;
}
.loadingVolumes {
position: relative;
top: 45%;
z-index: 100;
}
.table-container {
overflow: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
font-size: 13px;
@media(min-width: 576px){
font-size: 16px;
}
&::-webkit-scrollbar {
display: none;
}
}
.container-info{
overflow-x: scroll;
}

View File

@@ -0,0 +1,131 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable, combineLatest, BehaviorSubject, of } from 'rxjs';
import { map, share, switchMap } from 'rxjs/operators';
import { SeoService } from 'src/app/services/seo.service';
import { StateService } from 'src/app/services/state.service';
import { WebsocketService } from 'src/app/services/websocket.service';
import { BisqApiService } from '../bisq-api.service';
import { Trade } from '../bisq.interfaces';
@Component({
selector: 'app-bisq-dashboard',
templateUrl: './bisq-dashboard.component.html',
styleUrls: ['./bisq-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BisqDashboardComponent implements OnInit {
tickers$: Observable<any>;
volumes$: Observable<any>;
trades$: Observable<Trade[]>;
sort$ = new BehaviorSubject<string>('trades');
allowCryptoCoins = ['usdc', 'l-btc', 'bsq'];
constructor(
private websocketService: WebsocketService,
private bisqApiService: BisqApiService,
public stateService: StateService,
private seoService: SeoService,
) { }
ngOnInit(): void {
this.seoService.setTitle(`Markets`);
this.websocketService.want(['blocks']);
this.volumes$ = this.bisqApiService.getAllVolumesDay$()
.pipe(
map((volumes) => {
const data = volumes.map((volume) => {
return {
time: volume.period_start,
value: volume.volume,
};
});
const linesData = volumes.map((volume) => {
return {
time: volume.period_start,
value: volume.num_trades,
};
});
return {
data: data,
linesData: linesData,
};
})
);
const getMarkets = this.bisqApiService.getMarkets$().pipe(share());
this.tickers$ = combineLatest([
this.bisqApiService.getMarketsTicker$(),
getMarkets,
this.bisqApiService.getMarketVolumesByTime$('7d'),
])
.pipe(
map(([tickers, markets, volumes]) => {
const newTickers = [];
for (const t in tickers) {
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
const pair = t.split('_');
if (pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1) {
continue;
}
}
const mappedTicker: any = tickers[t];
mappedTicker.pair_url = t;
mappedTicker.pair = t.replace('_', '/').toUpperCase();
mappedTicker.market = markets[t];
mappedTicker.volume = volumes[t];
mappedTicker.name = `${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lname : mappedTicker.market.rname} (${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lsymbol : mappedTicker.market.rsymbol}`;
newTickers.push(mappedTicker);
}
return newTickers;
}),
switchMap((tickers) => combineLatest([this.sort$, of(tickers)])),
map(([sort, tickers]) => {
if (sort === 'trades') {
tickers.sort((a, b) => (b.volume && b.volume.num_trades || 0) - (a.volume && a.volume.num_trades || 0));
} else if (sort === 'volumes') {
tickers.sort((a, b) => (b.volume && b.volume.volume || 0) - (a.volume && a.volume.volume || 0));
} else if (sort === 'name') {
tickers.sort((a, b) => a.name.localeCompare(b.name));
}
return tickers;
})
);
this.trades$ = combineLatest([
this.bisqApiService.getMarketTrades$('all'),
getMarkets,
])
.pipe(
map(([trades, markets]) => {
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
trades = trades.filter((trade) => {
const pair = trade.market.split('_');
return !(pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1);
});
}
return trades.map((trade => {
trade._market = markets[trade.market];
return trade;
}));
})
);
}
trackByFn(index: number) {
return index;
}
sort(by: string) {
this.sort$.next(by);
}
}

View File

@@ -1 +0,0 @@
<router-outlet></router-outlet>

View File

@@ -1,18 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { WebsocketService } from 'src/app/services/websocket.service';
@Component({
selector: 'app-bisq-explorer',
templateUrl: './bisq-explorer.component.html',
styleUrls: ['./bisq-explorer.component.scss']
})
export class BisqExplorerComponent implements OnInit {
constructor(
private websocketService: WebsocketService,
) { }
ngOnInit(): void {
this.websocketService.want(['blocks']);
}
}

View File

@@ -0,0 +1,130 @@
<div class="container-xl">
<br>
<div class="row row-cols-1 row-cols-md-2">
<div class="col mb-4">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title" i18n="bisq-dashboard.price-index-title">Bisq Price Index</h5>
<div class="big-fiat">
<span *ngIf="usdPrice$ | async as usdPrice; else loading">
<span [appColoredPrice]="usdPrice">{{ usdPrice | currency:'USD':'symbol':'1.2-2' }}</span>
</span>
</div>
</div>
</div>
</div>
<div class="col mb-4">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title" i18n="bisq-dashboard.market-price-title">Bisq Market Price</h5>
<div class="big-fiat">
<span class="green-color" *ngIf="bisqMarketPrice; else loading">
<span [appColoredPrice]="bisqMarketPrice">{{ bisqMarketPrice | currency:'USD':'symbol':'1.2-2' }}</span>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2">
<div class="col mb-4">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">US Dollar - BTC/USD</h5>
<div class="chart-container">
<ng-container *ngIf="hlocData$ | async as hlocData; else loadingSpinner">
<app-lightweight-charts [height]="300" [data]="hlocData.hloc" [volumeData]="hlocData.volume" [precision]="2"></app-lightweight-charts>
</ng-container>
</div>
</div>
</div>
</div>
<div class="col mb-4">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title" i18n="Bisq markets title">Bisq Trading Volume</h5>
<div class="chart-container">
<ng-container *ngIf="volumes$ | async as volumes; else loadingSpinner">
<app-lightweight-charts-area [height]="300" [data]="volumes.data" [lineData]="volumes.linesData"></app-lightweight-charts-area>
</ng-container>
</div>
</div>
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2">
<ng-container *ngIf="{ value: (tickers$ | async) } as tickers">
<div class="col mb-4">
<div class="card">
<div class="card-body">
<h5 class="card-title text-center">
<ng-template [ngIf]="stateService.env.OFFICIAL_BISQ_MARKETS" [ngIfElse]="nonOfficialMarkets" i18n="Bisq All Markets">Markets</ng-template>
<ng-template #nonOfficialMarkets i18n="Bisq Bitcoin Markets">Bitcoin Markets</ng-template>
</h5>
<div class="table-container">
<table class="table table-borderless table-striped">
<thead>
<th><ng-container i18n>Currency</ng-container> <button [disabled]="(sort$ | async) === 'name'" class="btn btn-link btn-sm" (click)="sort('name')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
<th i18n>Price</th>
<th><ng-container i18n="Trades amount 7D">Trades (7d)</ng-container> <button [disabled]="(sort$ | async) === 'trades'" class="btn btn-link btn-sm" (click)="sort('trades')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
</thead>
<tbody *ngIf="tickers.value; else loadingTmpl">
<tr *ngFor="let ticker of tickers.value; trackBy: trackByFn;">
<td><a [routerLink]="['/market' | relativeUrl, ticker.pair_url]">{{ ticker.name }})</a></td>
<td>
<app-fiat *ngIf="ticker.market.rtype === 'crypto'; else fiat" [value]="ticker.last * 100000000"></app-fiat>
<ng-template #fiat>
<span class="green-color">{{ ticker.last | currency: ticker.market.rsymbol }}</span>
</ng-template>
</td>
<td>{{ ticker.volume?.num_trades ? ticker.volume?.num_trades : 0 }}</td>
</tr>
</tbody>
</table>
</div>
<div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-all">View all &raquo;</a></div>
</div>
</div>
</div>
<div class="col mb-4">
<div class="card">
<div class="card-body">
<h5 class="card-title text-center" i18n="Latest Trades header">Latest Trades</h5>
<app-bisq-trades [trades$]="trades$" view="small"></app-bisq-trades>
<div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-all">View all &raquo;</a></div>
</div>
</div>
</div>
</ng-container>
</div>
<app-language-selector *ngIf="!stateService.env.OFFICIAL_BISQ_MARKETS"></app-language-selector>
<div class="text-small text-center mt-3">
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
</div>
</div>
<ng-template #loadingTmpl>
<tr *ngFor="let i of [1,2,3,4,5,6,7,8,9,10]">
<td *ngFor="let j of [1, 2, 3]"><span class="skeleton-loader"></span></td>
</tr>
</ng-template>
<ng-template #loadingSpinner>
<div class="text-center loadingGraphs">
<div class="spinner-border text-light"></div>
</div>
</ng-template>
<ng-template #loading>
<div class="skeleton-loader shorter"></div>
</ng-template>

View File

@@ -0,0 +1,84 @@
#volumeHolder {
height: 500px;
background-color: #000;
overflow: hidden;
display: flex;
justify-content: center;
}
.table {
max-width: 100%;
overflow: scroll;
}
.loadingGraphs {
position: relative;
top: 45%;
z-index: 100;
}
.table-container {
font-size: 13px;
@media(min-width: 576px){
font-size: 16px;
}
&::-webkit-scrollbar {
display: none;
}
}
.chart-container {
height: 300px;
}
.big-fiat {
color: #3bcc49;
font-size: 26px;
}
.card {
background-color: #1d1f31;
height: 100%;
}
.card-title {
color: #4a68b9;
font-size: 1rem;
}
.info-block {
float: left;
width: 350px;
line-height: 25px;
}
.progress {
display: inline-flex;
width: 100%;
background-color: #2d3348;
height: 1.1rem;
}
.bg-warning {
background-color: #b58800 !important;
}
.skeleton-loader {
max-width: 100%;
&.shorter {
max-width: 150px;
}
}
.more-padding {
padding: 1.25rem 2rem 1.25rem 2rem;
}
.graph-card {
height: 100%;
@media (min-width: 992px) {
height: 385px;
}
}

View File

@@ -0,0 +1,192 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable, combineLatest, BehaviorSubject, of } from 'rxjs';
import { map, share, switchMap } from 'rxjs/operators';
import { SeoService } from 'src/app/services/seo.service';
import { StateService } from 'src/app/services/state.service';
import { WebsocketService } from 'src/app/services/websocket.service';
import { BisqApiService } from '../bisq-api.service';
import { Trade } from '../bisq.interfaces';
@Component({
selector: 'app-main-bisq-dashboard',
templateUrl: './bisq-main-dashboard.component.html',
styleUrls: ['./bisq-main-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BisqMainDashboardComponent implements OnInit {
tickers$: Observable<any>;
volumes$: Observable<any>;
trades$: Observable<Trade[]>;
sort$ = new BehaviorSubject<string>('trades');
hlocData$: Observable<any>;
usdPrice$: Observable<number>;
isLoadingGraph = true;
bisqMarketPrice = 0;
allowCryptoCoins = ['usdc', 'l-btc', 'bsq'];
constructor(
private websocketService: WebsocketService,
private bisqApiService: BisqApiService,
public stateService: StateService,
private seoService: SeoService,
) { }
ngOnInit(): void {
this.seoService.setTitle(`Markets`);
this.websocketService.want(['blocks']);
this.usdPrice$ = this.stateService.conversions$.asObservable().pipe(
map((conversions) => conversions.USD)
);
this.volumes$ = this.bisqApiService.getAllVolumesDay$()
.pipe(
map((volumes) => {
const data = volumes.map((volume) => {
return {
time: volume.period_start,
value: volume.volume,
};
});
const linesData = volumes.map((volume) => {
return {
time: volume.period_start,
value: volume.num_trades,
};
});
return {
data: data,
linesData: linesData,
};
})
);
const getMarkets = this.bisqApiService.getMarkets$().pipe(share());
this.tickers$ = combineLatest([
this.bisqApiService.getMarketsTicker$(),
getMarkets,
this.bisqApiService.getMarketVolumesByTime$('7d'),
])
.pipe(
map(([tickers, markets, volumes]) => {
const newTickers = [];
for (const t in tickers) {
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
const pair = t.split('_');
if (pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1) {
continue;
}
}
const mappedTicker: any = tickers[t];
mappedTicker.pair_url = t;
mappedTicker.pair = t.replace('_', '/').toUpperCase();
mappedTicker.market = markets[t];
mappedTicker.volume = volumes[t];
mappedTicker.name = `${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lname : mappedTicker.market.rname} (${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lsymbol : mappedTicker.market.rsymbol}`;
newTickers.push(mappedTicker);
}
return newTickers;
}),
switchMap((tickers) => combineLatest([this.sort$, of(tickers)])),
map(([sort, tickers]) => {
if (sort === 'trades') {
tickers.sort((a, b) => (b.volume && b.volume.num_trades || 0) - (a.volume && a.volume.num_trades || 0));
} else if (sort === 'volumes') {
tickers.sort((a, b) => (b.volume && b.volume.volume || 0) - (a.volume && a.volume.volume || 0));
} else if (sort === 'name') {
tickers.sort((a, b) => a.name.localeCompare(b.name));
}
return tickers.slice(0, 10);
})
);
this.trades$ = combineLatest([
this.bisqApiService.getMarketTrades$('all'),
getMarkets,
])
.pipe(
map(([trades, markets]) => {
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
trades = trades.filter((trade) => {
const pair = trade.market.split('_');
return !(pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1);
});
}
return trades.map((trade => {
trade._market = markets[trade.market];
return trade;
})).slice(0, 10);
})
);
this.hlocData$ = this.bisqApiService.getMarketsHloc$('btc_usd', 'day')
.pipe(
map((hlocData) => {
this.isLoadingGraph = false;
hlocData = hlocData.map((h) => {
h.time = h.period_start;
return h;
});
const hlocVolume = hlocData.map((h) => {
return {
time: h.time,
value: h.volume_right,
color: h.close > h.avg ? 'rgba(0, 41, 74, 0.7)' : 'rgba(0, 41, 74, 1)',
};
});
// Add whitespace
if (hlocData.length > 1) {
const newHloc = [];
newHloc.push(hlocData[0]);
const period = 86400;
let periods = 0;
const startingDate = hlocData[0].period_start;
let index = 1;
while (true) {
periods++;
if (hlocData[index].period_start > startingDate + period * periods) {
newHloc.push({
time: startingDate + period * periods,
});
} else {
newHloc.push(hlocData[index]);
index++;
if (!hlocData[index]) {
break;
}
}
}
hlocData = newHloc;
}
this.bisqMarketPrice = hlocData[hlocData.length - 1].close;
return {
hloc: hlocData,
volume: hlocVolume,
};
}),
);
}
trackByFn(index: number) {
return index;
}
sort(by: string) {
this.sort$.next(by);
}
}

View File

@@ -0,0 +1,112 @@
<div class="container-xl">
<ng-container *ngIf="hlocData$ | async as hlocData; else loadingSpinner">
<ng-container *ngIf="currency$ | async as currency; else loadingSpinner">
<h1>{{ currency.market.rtype === 'crypto' ? currency.market.lname : currency.market.rname }} - {{ currency.pair }}</h1>
<div class="priceheader">
<ng-container *ngIf="currency.market.rtype === 'fiat'; else headerPriceCrypto"><span class="green-color">{{ hlocData.hloc[hlocData.hloc.length - 1].close | currency: currency.market.rsymbol }}</span></ng-container>
<ng-template #headerPriceCrypto>{{ hlocData.hloc[hlocData.hloc.length - 1].close | number: '1.' + currency.market.rprecision + '-' + currency.market.rprecision }} {{ currency.market.rsymbol }}</ng-template>
</div>
<form [formGroup]="radioGroupForm" class="mb-3 radio-form">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="interval">
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'half_hour'" (click)="setFragment('half_hour')"> 30M
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'hour'" (click)="setFragment('hour')"> 1H
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'half_day'" (click)="setFragment('half_day')"> 12H
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'day'" (click)="setFragment('day')"> 1D
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'week'" (click)="setFragment('week')"> 1W
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'month'" (click)="setFragment('month')"> 1M
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'year'" (click)="setFragment('year')"> 1Y
</label>
</div>
</form>
<div class="clearfix"></div>
<div id="graphHolder">
<div class="text-center loadingChart" [hidden]="!isLoadingGraph">
<div class="spinner-border text-light"></div>
</div>
<app-lightweight-charts [data]="hlocData.hloc" [volumeData]="hlocData.volume" [precision]="currency.market.rtype === 'crypto' ? currency.market.lprecision : currency.market.rprecision"></app-lightweight-charts>
</div>
<br>
<ng-container *ngIf="offers$ | async as offers; else loadingSpinner">
<div class="row row-cols-1 row-cols-md-2">
<ng-container *ngTemplateOutlet="offersList; context: { offers: offers.buys, direction: 'BUY', market: currency.market }"></ng-container>
<ng-container *ngTemplateOutlet="offersList; context: { offers: offers.sells, direction: 'SELL', market: currency.market }"></ng-container>
</div>
</ng-container>
<br><br>
<ng-container *ngIf="trades$ | async as trades; else loadingSpinner">
<h2 i18n="Latest Trades header">Latest Trades</h2>
<app-bisq-trades [trades$]="trades$" [market]="currency.market"></app-bisq-trades>
</ng-container>
</ng-container>
</ng-container>
</div>
<ng-template #offersList let-offers="offers" let-direction="direction", let-market="market">
<div class="col">
<h2>
<ng-template [ngIf]="direction === 'BUY'" [ngIfElse]="sellOffers" i18n="Bisq Buy Offers">Buy Offers</ng-template>
<ng-template #sellOffers i18n="Bisq Sell Offers">Sell Offers</ng-template>
</h2>
<div class="table-container">
<table class="table table-borderless table-striped">
<thead>
<th i18n>Price</th>
<th><ng-container *ngTemplateOutlet="amount; context: {$implicit: market.lsymbol }"></ng-container></th>
<th><ng-container *ngTemplateOutlet="amount; context: {$implicit: market.rsymbol }"></ng-container></th>
</thead>
<tbody>
<tr *ngFor="let offer of offers">
<td>
<ng-container *ngIf="market.rtype === 'fiat'; else priceCrypto"><span class="green-color">{{ offer.price | currency: market.rsymbol }}</span></ng-container>
<ng-template #priceCrypto>{{ offer.price | number: '1.2-' + market.rprecision }} <span class="symbol">{{ market.rsymbol }}</span></ng-template>
</td>
<td>
<ng-container *ngIf="market.ltype === 'fiat'; else amountCrypto"><span class="green-color">{{ offer.amount | currency: market.rsymbol }}</span></ng-container>
<ng-template #amountCrypto>{{ offer.amount | number: '1.2-' + market.lprecision }} <span class="symbol">{{ market.lsymbol }}</span></ng-template>
</td>
<td>
<ng-container *ngIf="market.rtype === 'fiat'; else volumeCrypto"><span class="green-color">{{ offer.volume | currency: market.rsymbol }}</span></ng-container>
<ng-template #volumeCrypto>{{ offer.volume | number: '1.2-' + market.rprecision }} <span class="symbol">{{ market.rsymbol }}</span></ng-template>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</ng-template>
<ng-template #loadingSpinner>
<br>
<br>
<div class="text-center">
<div class="spinner-border text-light"></div>
</div>
</ng-template>
<ng-template #amount let-i i18n="Trade amount (Symbol)">Amount ({{ i }})</ng-template>

View File

@@ -0,0 +1,46 @@
.priceheader {
font-size: 24px;
@media(min-width: 576px){
float: left;
}
}
.radio-form {
@media(min-width: 576px){
float: right;
}
}
.loadingChart {
z-index: 100;
position: absolute;
top: 50%;
left: 50%;
}
#graphHolder {
height: 550px;
overflow: hidden;
}
.col {
&:last-child{
margin-top: 50px;
@media(min-width: 576px){
margin-top: 0px;
}
}
}
.table-container {
overflow: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
font-size: 13px;
@media(min-width: 576px){
font-size: 16px;
}
&::-webkit-scrollbar {
display: none;
}
}

View File

@@ -0,0 +1,158 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { combineLatest, merge, Observable, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { SeoService } from 'src/app/services/seo.service';
import { WebsocketService } from 'src/app/services/websocket.service';
import { BisqApiService } from '../bisq-api.service';
import { OffersMarket, Trade } from '../bisq.interfaces';
@Component({
selector: 'app-bisq-market',
templateUrl: './bisq-market.component.html',
styleUrls: ['./bisq-market.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BisqMarketComponent implements OnInit, OnDestroy {
hlocData$: Observable<any>;
currency$: Observable<any>;
offers$: Observable<OffersMarket>;
trades$: Observable<Trade[]>;
radioGroupForm: FormGroup;
defaultInterval = 'day';
isLoadingGraph = false;
constructor(
private websocketService: WebsocketService,
private route: ActivatedRoute,
private bisqApiService: BisqApiService,
private formBuilder: FormBuilder,
private seoService: SeoService,
private router: Router,
) { }
ngOnInit(): void {
this.radioGroupForm = this.formBuilder.group({
interval: [this.defaultInterval],
});
if (['half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto'].indexOf(this.route.snapshot.fragment) > -1) {
this.radioGroupForm.controls.interval.setValue(this.route.snapshot.fragment, { emitEvent: false });
}
this.currency$ = this.bisqApiService.getMarkets$()
.pipe(
switchMap((markets) => combineLatest([of(markets), this.route.paramMap])),
map(([markets, routeParams]) => {
const pair = routeParams.get('pair');
const pairUpperCase = pair.replace('_', '/').toUpperCase();
this.seoService.setTitle(`Bisq market: ${pairUpperCase}`);
return {
pair: pairUpperCase,
market: markets[pair],
};
})
);
this.trades$ = this.route.paramMap
.pipe(
map(routeParams => routeParams.get('pair')),
switchMap((marketPair) => this.bisqApiService.getMarketTrades$(marketPair)),
);
this.offers$ = this.route.paramMap
.pipe(
map(routeParams => routeParams.get('pair')),
switchMap((marketPair) => this.bisqApiService.getMarketOffers$(marketPair)),
map((offers) => offers[Object.keys(offers)[0]])
);
this.hlocData$ = combineLatest([
this.route.paramMap,
merge(this.radioGroupForm.get('interval').valueChanges, of(this.radioGroupForm.get('interval').value)),
])
.pipe(
switchMap(([routeParams, interval]) => {
this.isLoadingGraph = true;
const pair = routeParams.get('pair');
return this.bisqApiService.getMarketsHloc$(pair, interval);
}),
map((hlocData) => {
this.isLoadingGraph = false;
hlocData = hlocData.map((h) => {
h.time = h.period_start;
return h;
});
const hlocVolume = hlocData.map((h) => {
return {
time: h.time,
value: h.volume_right,
color: h.close > h.avg ? 'rgba(0, 41, 74, 0.7)' : 'rgba(0, 41, 74, 1)',
};
});
// Add whitespace
if (hlocData.length > 1) {
const newHloc = [];
newHloc.push(hlocData[0]);
const period = this.getUnixTimestampFromInterval(this.radioGroupForm.get('interval').value); // temp
let periods = 0;
const startingDate = hlocData[0].period_start;
let index = 1;
while (true) {
periods++;
if (hlocData[index].period_start > startingDate + period * periods) {
newHloc.push({
time: startingDate + period * periods,
});
} else {
newHloc.push(hlocData[index]);
index++;
if (!hlocData[index]) {
break;
}
}
}
hlocData = newHloc;
}
return {
hloc: hlocData,
volume: hlocVolume,
};
}),
);
}
setFragment(fragment: string) {
this.router.navigate([], {
relativeTo: this.route,
queryParamsHandling: 'merge',
fragment: fragment
});
}
ngOnDestroy(): void {
this.websocketService.stopTrackingBisqMarket();
}
getUnixTimestampFromInterval(interval: string): number {
switch (interval) {
case 'minute': return 60;
case 'half_hour': return 1800;
case 'hour': return 3600;
case 'half_day': return 43200;
case 'day': return 86400;
case 'week': return 604800;
case 'month': return 2592000;
case 'year': return 31579200;
}
}
}

View File

@@ -5,20 +5,20 @@
<div class="clearfix"></div>
<div class="row">
<div class="col-sm">
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody *ngIf="!isLoading; else loadingTemplate">
<tr>
<td class="td-width" i18n="BSQ existing amount">Existing amount</td>
<td>{{ (stats.minted - stats.burnt) | number: '1.2-2' }} BSQ</td>
<td>{{ (stats.minted - stats.burnt) | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="BSQ minted amount">Minted amount</td>
<td>{{ stats.minted | number: '1.2-2' }} BSQ</td>
<td>{{ stats.minted | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="BSQ burnt amount">Burnt amount</td>
<td>{{ stats.burnt | number: '1.2-2' }} BSQ</td>
<td>{{ stats.burnt | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="BSQ addresses">Addresses</td>
@@ -33,7 +33,7 @@
<td>{{ stats.spent_txos | number }}</td>
</tr>
<tr>
<td i18n="BSQ token price">Price</td>
<td i18n>Price</td>
<td><app-fiat [value]="price"></app-fiat></td>
</tr>
<tr>
@@ -44,7 +44,7 @@
</table>
</div>
<div class="col-sm"></div>
<div class="col-md"></div>
</div>
</div>

View File

@@ -7,3 +7,12 @@
width: 175px;
}
}
.fiat {
display: block;
font-size: 13px;
@media (min-width: 768px) {
font-size: 14px;
display: inline-block;
}
}

View File

@@ -3,6 +3,7 @@ import { BisqApiService } from '../bisq-api.service';
import { BisqStats } from '../bisq.interfaces';
import { SeoService } from 'src/app/services/seo.service';
import { StateService } from 'src/app/services/state.service';
import { WebsocketService } from 'src/app/services/websocket.service';
@Component({
selector: 'app-bisq-stats',
@@ -15,12 +16,15 @@ export class BisqStatsComponent implements OnInit {
price: number;
constructor(
private websocketService: WebsocketService,
private bisqApiService: BisqApiService,
private seoService: SeoService,
private stateService: StateService,
) { }
ngOnInit() {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@2a30a4cdb123a03facc5ab8c5b3e6d8b8dbbc3d4:BSQ statistics`);
this.stateService.bsqPrice$
.subscribe((bsqPrice) => {

View File

@@ -0,0 +1,46 @@
<div class="table-container">
<table class="table table-borderless table-striped">
<thead>
<th i18n>Date</th>
<th *ngIf="view === 'all'" i18n>Price</th>
<th><ng-container *ngTemplateOutlet="amount; context: {$implicit: 'BTC' }"></ng-container></th>
<th>
<ng-template [ngIf]="market" [ngIfElse]="noMarket"><ng-container *ngTemplateOutlet="amount; context: {$implicit: market.lsymbol === 'BTC' ? market.rsymbol : market.lsymbol }"></ng-container></ng-template>
<ng-template #noMarket i18n>Amount</ng-template>
</th>
</thead>
<tbody *ngIf="(trades$ | async) as trades; else loadingTmpl">
<tr *ngFor="let trade of trades;">
<td>
{{ 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>
<ng-template #priceCrypto>{{ trade.price | number: '1.2-' + (trade._market || market).rprecision }} <span class="symbol">{{ (trade._market || market).rsymbol }}</span></ng-template>
</td>
<ng-container *ngTemplateOutlet="(trade._market || market).rsymbol === 'BTC' ? tradeVolume : tradeAmount"></ng-container>
<ng-container *ngTemplateOutlet="(trade._market || market).rsymbol === 'BTC' ? tradeAmount : tradeVolume"></ng-container>
<ng-template #tradeAmount>
<td>
<ng-container *ngIf="(trade._market || market).ltype === 'fiat'; else amountCrypto"><span class="green-color">{{ trade.amount | currency: (trade._market || market).rsymbol }}</span></ng-container>
<ng-template #amountCrypto>{{ trade.amount | number: '1.2-' + (trade._market || market).lprecision }} <span class="symbol">{{ (trade._market || market).lsymbol }}</span></ng-template>
</td>
</ng-template>
<ng-template #tradeVolume>
<td>
<ng-container *ngIf="(trade._market || market).rtype === 'fiat'; else volumeCrypto"><span class="green-color">{{ trade.volume | currency: (trade._market || market).rsymbol }}</span></ng-container>
<ng-template #volumeCrypto>{{ trade.volume | number: '1.2-' + (trade._market || market).rprecision }} <span class="symbol">{{ (trade._market || market).rsymbol }}</span></ng-template>
</td>
</ng-template>
</tr>
</tbody>
</table>
</div>
<ng-template #loadingTmpl>
<tr *ngFor="let i of [1,2,3,4,5,6,7,8,9,10]">
<td *ngFor="let j of loadingColumns"><span class="skeleton-loader"></span></td>
</tr>
</ng-template>
<ng-template #amount let-i i18n="Trade amount (Symbol)">Amount ({{ i }})</ng-template>

View File

@@ -0,0 +1,12 @@
.table-container {
overflow: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
font-size: 13px;
@media(min-width: 576px){
font-size: 16px;
}
&::-webkit-scrollbar {
display: none;
}
}

View File

@@ -0,0 +1,22 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
@Component({
selector: 'app-bisq-trades',
templateUrl: './bisq-trades.component.html',
styleUrls: ['./bisq-trades.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BisqTradesComponent implements OnChanges {
@Input() trades$: Observable<any>;
@Input() market: any;
@Input() view: 'all' | 'small' = 'all';
loadingColumns = [1, 2, 3, 4];
ngOnChanges() {
if (this.view === 'small') {
this.loadingColumns = [1, 2, 3];
}
}
}

View File

@@ -1,24 +1,24 @@
<div class="box">
<div class="row">
<div class="col-sm">
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="transaction.inputs">Inputs</td>
<td>{{ totalInput / 100 | number: '1.2-2' }} BSQ</td>
<td>{{ totalInput / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="transaction.outputs">Outputs</td>
<td>{{ totalOutput / 100 | number: '1.2-2' }} BSQ</td>
<td>{{ totalOutput / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="asset.issued-amount|Liquid Asset issued amount">Issued amount</td>
<td>{{ totalIssued / 100 | number: '1.2-2' }} BSQ</td>
<td>{{ totalIssued / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
</tbody>
</table>
</div>
<div class="col-sm">
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody class="mobile-even">
<tr>

View File

@@ -8,4 +8,15 @@
.mobile-even tr:nth-of-type(odd) {
background-color: inherit;
}
}
.table {
tr td {
&:last-child{
text-align: right;
@media(min-width: 768px){
text-align: left;
}
}
}
}

View File

@@ -1,16 +1,18 @@
<div class="container-xl">
<h1 class="float-left mr-3 mb-md-3" i18n="shared.transaction">Transaction</h1>
<ng-template [ngIf]="!isLoading && !error">
<button *ngIf="(latestBlock$ | async) as latestBlock" type="button" class="btn btn-sm btn-success float-right mr-2 mt-1 mt-md-3">
<button *ngIf="(latestBlock$ | async) as latestBlock" type="button" class="btn btn-sm btn-success float-right">
<ng-container *ngTemplateOutlet="latestBlock.height - bisqTx.blockHeight + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - bisqTx.blockHeight + 1}"></ng-container>
<ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
</button>
<div>
<a [routerLink]="['/bisq-tx' | relativeUrl, bisqTx.id]" style="line-height: 56px;">
<h1 i18n="shared.transaction">Transaction</h1>
<div class="tx-link">
<a [routerLink]="['/bisq-tx' | relativeUrl, bisqTx.id]">
<span class="d-inline d-lg-none">{{ bisqTx.id | shortenString : 24 }}</span>
<span class="d-none d-lg-inline">{{ bisqTx.id }}</span>
</a>
@@ -18,7 +20,7 @@
</div>
<div class="clearfix"></div>
<div class="box">
<div class="box transaction-container">
<div class="row">
<div class="col-sm">
<table class="table table-borderless table-striped">
@@ -28,7 +30,7 @@
<td>
{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
<i>(<app-time-since [time]="bisqTx.time / 1000" [fastRender]="true"></app-time-since>)</i>
<i class="symbol">(<app-time-since [time]="bisqTx.time / 1000" [fastRender]="true"></app-time-since>)</i>
</div>
</td>
</tr>
@@ -56,12 +58,12 @@
<tr>
<td class="td-width" i18n="BSQ burnt amount">Burnt amount</td>
<td>
{{ bisqTx.burntFee / 100 | number: '1.2-2' }} BSQ (<app-bsq-amount [bsq]="bisqTx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount>)
{{ bisqTx.burntFee / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span> <span class="fiat"><app-bsq-amount [bsq]="bisqTx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount></span>
</tr>
<tr>
<td i18n="transaction.fee-per-vbyte|Transaction fee">Fee per vByte</td>
<td *ngIf="!isLoadingTx; else loadingTxFee">
{{ tx.fee / (tx.weight / 4) | number : '1.1-1' }} sat/vB
{{ tx.fee / (tx.weight / 4) | number : '1.1-1' }} <span class="symbol">sat/vB</span>
&nbsp;
<app-tx-fee-rating [tx]="tx"></app-tx-fee-rating>
</td>

View File

@@ -1,9 +1,126 @@
.td-width {
width: 175px;
.adjust-btn-padding {
padding: 0.55rem;
}
.title-block {
padding-top: 1px;
}
@media (max-width: 767.98px) {
.td-width {
width: 150px;
.mobile-bottomcol {
margin-top: 15px;
}
}
.td-width {
width: 150px;
@media (min-width: 768px) {
width: 175px;
}
}
h1 {
margin-bottom: 0;
}
.badge {
position: relative;
top: -1px;
}
.btn-small-height {
line-height: 1.1;
}
.arrow-green {
color: #1a9436;
}
.arrow-red {
color: #dc3545;
}
.btn-success {
float: right;
margin-bottom: 0px;
margin-top: 5px;
}
.tx-link {
display: block;
width: auto;
margin-bottom: 10px;
margin-left: 2px;
margin-top: 40px;
position: absolute;
@media (min-width: 700px) {
margin-top: 14px;
margin-left: 10px;
position: relative;
text-align: left;
width: 100%;
top: 14px;
left: 10px;
}
}
.container-xl {
margin-bottom: 40px;
}
.row{
flex-direction: column;
@media (min-width: 768px) {
flex-direction: row;
}
}
@media (max-width: 767.98px) {
.mobile-bottomcol {
margin-top: 15px;
}
.details-table td:first-child {
white-space: pre-wrap;
}
}
.fiat {
display: block;
font-size: 13px;
@media (min-width: 576px){
display: inline-block;
margin-left: 15px;
font-size: 14px;
text-align: left;
}
}
h1{
font-size: 1.75rem;
margin-top: 2px;
margin-bottom: 0;
float: left;
padding-bottom: 40px;
@media (min-width: 375px){
margin-top: 0px;
font-size: 2rem;
}
@media (min-width: 768px){
padding-bottom: 10px;
}
}
.transaction-container {
font-size: 14px;
@media (min-width: 576px){
font-size: 16px;
}
tr td {
&:last-child{
text-align: right;
@media (min-width: 768px){
text-align: left;
}
}
}
}

View File

@@ -9,6 +9,7 @@ import { BisqApiService } from '../bisq-api.service';
import { SeoService } from 'src/app/services/seo.service';
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
import { HttpErrorResponse } from '@angular/common/http';
import { WebsocketService } from 'src/app/services/websocket.service';
@Component({
selector: 'app-bisq-transaction',
@@ -27,6 +28,7 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
subscription: Subscription;
constructor(
private websocketService: WebsocketService,
private route: ActivatedRoute,
private bisqApiService: BisqApiService,
private electrsApiService: ElectrsApiService,
@@ -36,6 +38,8 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
) { }
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.subscription = this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
this.isLoading = true;

View File

@@ -1,5 +1,5 @@
<div class="container-xl">
<h1 style="float: left;" i18n>Transactions</h1>
<h1 style="float: left;" i18n>BSQ Transactions</h1>
<div class="d-block float-right">
<form [formGroup]="radioGroupForm">
@@ -15,7 +15,7 @@
<table class="table table-borderless table-striped">
<thead>
<th style="width: 20%;" i18n>Transaction</th>
<th style="width: 20%;" i18n>TXID</th>
<th class="d-none d-md-block" style="width: 100%;" i18n>Type</th>
<th style="width: 20%;" i18n>Amount</th>
<th style="width: 20%;" i18n>Confirmed</th>
@@ -26,15 +26,15 @@
<td><a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ data: tx }">{{ tx.id | slice : 0 : 8 }}</a></td>
<td class="d-none d-md-block">
<app-bisq-icon class="mr-1" [txType]="tx.txType"></app-bisq-icon>
<span class="d-none d-md-inline"> {{ tx.txTypeDisplayString }}</span>
<span class="d-none d-md-inline"> {{ getStringByTxType(tx.txType) }}</span>
</td>
<td>
<app-bisq-icon class="d-inline d-md-none mr-1" [txType]="tx.txType"></app-bisq-icon>
<ng-template [ngIf]="tx.txType === 'PAY_TRADE_FEE' || tx.txType === 'ASSET_LISTING_FEE'" [ngIfElse]="defaultTxType">
{{ tx.burntFee / 100 | number: '1.2-2' }}<span class="d-none d-md-inline"> BSQ</span>
{{ tx.burntFee / 100 | number: '1.2-2' }} <span class="d-none d-md-inline symbol">BSQ</span>
</ng-template>
<ng-template #defaultTxType>
{{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }}<span class="d-none d-md-inline"> BSQ</span>
{{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }} <span class="d-none d-md-inline symbol">BSQ</span>
</ng-template>
</td>
<td><app-time-since [time]="tx.time / 1000" [fastRender]="true"></app-time-since></td>
@@ -44,8 +44,9 @@
</table>
<br>
<ngb-pagination *ngIf="transactions.value" [size]="paginationSize" [collectionSize]="transactions.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true"></ngb-pagination>
<div class="pagination">
<ngb-pagination *ngIf="transactions.value" [size]="paginationSize" [collectionSize]="transactions.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true"></ngb-pagination>
</div>
</ng-container>
</div>
@@ -54,4 +55,4 @@
<tr *ngFor="let i of loadingItems">
<td *ngFor="let j of [1, 2, 3, 4, 5]"><span class="skeleton-loader"></span></td>
</tr>
</ng-template>
</ng-template>

View File

@@ -7,3 +7,14 @@ label {
right: 0px;
left: inherit;
}
.pagination {
overflow: hidden;
}
h1{
font-size: 1.5rem;
@media(min-width: 375px){
font-size: 2rem;
}
}

View File

@@ -8,6 +8,7 @@ import { SeoService } from 'src/app/services/seo.service';
import { FormGroup, FormBuilder } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router';
import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from 'ngx-bootrap-multiselect';
import { WebsocketService } from 'src/app/services/websocket.service';
@Component({
selector: 'app-bisq-transactions',
@@ -26,19 +27,19 @@ export class BisqTransactionsComponent implements OnInit {
types: string[] = [];
txTypeOptions: IMultiSelectOption[] = [
{ id: 1, name: 'Asset listing fee' },
{ id: 2, name: 'Blind vote' },
{ id: 3, name: 'Compensation request' },
{ id: 4, name: 'Genesis' },
{ id: 13, name: 'Irregular' },
{ id: 5, name: 'Lockup' },
{ id: 6, name: 'Pay trade fee' },
{ id: 7, name: 'Proof of burn' },
{ id: 8, name: 'Proposal' },
{ id: 9, name: 'Reimbursement request' },
{ id: 10, name: 'Transfer BSQ' },
{ id: 11, name: 'Unlock' },
{ id: 12, name: 'Vote reveal' },
{ id: 1, name: $localize`Asset listing fee` },
{ id: 2, name: $localize`Blind vote` },
{ id: 3, name: $localize`Compensation request` },
{ id: 4, name: $localize`Genesis` },
{ id: 13, name: $localize`Irregular` },
{ id: 5, name: $localize`Lockup` },
{ id: 6, name: $localize`Pay trade fee` },
{ id: 7, name: $localize`Proof of burn` },
{ id: 8, name: $localize`Proposal` },
{ id: 9, name: $localize`Reimbursement request` },
{ id: 10, name: $localize`Transfer BSQ` },
{ id: 11, name: $localize`Unlock` },
{ id: 12, name: $localize`Vote reveal` },
];
txTypesDefaultChecked = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
@@ -59,12 +60,13 @@ export class BisqTransactionsComponent implements OnInit {
// @ts-ignore
paginationSize: 'sm' | 'lg' = 'md';
paginationMaxSize = 10;
paginationMaxSize = 4;
txTypes = ['ASSET_LISTING_FEE', 'BLIND_VOTE', 'COMPENSATION_REQUEST', 'GENESIS', 'LOCKUP', 'PAY_TRADE_FEE',
'PROOF_OF_BURN', 'PROPOSAL', 'REIMBURSEMENT_REQUEST', 'TRANSFER_BSQ', 'UNLOCK', 'VOTE_REVEAL', 'IRREGULAR'];
constructor(
private websocketService: WebsocketService,
private bisqApiService: BisqApiService,
private seoService: SeoService,
private formBuilder: FormBuilder,
@@ -74,6 +76,7 @@ export class BisqTransactionsComponent implements OnInit {
) { }
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@add4cd82e3e38a3110fe67b3c7df56e9602644ee:Transactions`);
this.radioGroupForm = this.formBuilder.group({
@@ -157,6 +160,11 @@ export class BisqTransactionsComponent implements OnInit {
return outputs.reduce((acc: number, output: BisqOutput) => acc + output.bsqAmount, 0);
}
getStringByTxType(type: string) {
const id = this.txTypes.indexOf(type) + 1;
return this.txTypeOptions.find((type) => id === type.id).name;
}
trackByFn(index: number) {
return index;
}

View File

@@ -58,11 +58,11 @@
</div>
<div>
<div class="float-left mt-2-5" *ngIf="showConfirmations && tx.burntFee">
<ng-container i18n="BSQ burnt amount">Burnt amount</ng-container>: {{ tx.burntFee / 100 | number: '1.2-2' }} BSQ (<app-bsq-amount [bsq]="tx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount>)
<div class="transaction-fee" *ngIf="showConfirmations && tx.burntFee">
<ng-container i18n="BSQ burnt amount">Burnt amount</ng-container>: {{ tx.burntFee / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span> <span class="extra-info"><span class="fiat"><app-bsq-amount [bsq]="tx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount></span></span>
</div>
<div class="float-right">
<div class="btn-container">
<span *ngIf="showConfirmations && latestBlock$ | async as latestBlock">
<button type="button" class="btn btn-sm btn-success mt-2">
<ng-container *ngTemplateOutlet="latestBlock.height - tx.blockHeight + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - tx.blockHeight + 1}"></ng-container>

View File

@@ -59,26 +59,81 @@
background-color:#6c757d;
}
.scriptmessage {
max-width: 280px;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
}
.scriptmessage.longer {
max-width: 500px;
}
@media (max-width: 767.98px) {
.mobile-bottomcol {
margin-top: 15px;
}
.scriptmessage {
max-width: 90px !important;
.details-table td:first-child {
white-space: pre-wrap;
}
.scriptmessage.longer {
max-width: 280px !important;
}
.details-table {
margin-top: 5px;
}
.details-table td {
padding: 0.75rem;
}
.details-table td:nth-child(2) {
word-break: break-all;
white-space: normal;
font-family: "Courier New", Courier, monospace;
font-size: 12px;
}
.smaller-text {
font-size: 12px;
@media (min-width: 576px) {
font-size: 14px !important;
}
}
.longer {
max-width: 100% !important;
width: 200px;
display: inline-block;
}
.row{
flex-direction: column;
@media (min-width: 992px) {
flex-direction: row;
}
}
.extra-info {
display: inline-table;
.fiat {
font-size: 14px;
display: block;
text-align: right;
}
}
.transaction-fee {
display: block;
margin: 0px auto 5px;
@media (min-width: 576px) {
display: inline-table;
}
}
.fiat {
margin-left: 10px;
font-size: 13px;
@media (min-width: 576px) {
font-size: 14px;
}
}
.btn-container {
text-align: right;
@media (min-width: 576px) {
display: inline-table;
float: right;
}
}

View File

@@ -80,3 +80,182 @@ interface SpentInfo {
inputIndex: number;
txId: string;
}
export interface BisqTrade {
direction: string;
price: string;
amount: string;
volume: string;
payment_method: string;
trade_id: string;
trade_date: number;
market?: string;
}
export interface Currencies { [txid: string]: Currency; }
export interface Currency {
code: string;
name: string;
precision: number;
_type: string;
}
export interface Depth { [market: string]: Market; }
interface Market {
'buys': string[];
'sells': string[];
}
export interface HighLowOpenClose {
period_start: number | string;
open: string;
high: string;
low: string;
close: string;
volume_left: string;
volume_right: string;
avg: string;
}
export interface Markets { [txid: string]: Pair; }
interface Pair {
pair: string;
lname: string;
rname: string;
lsymbol: string;
rsymbol: string;
lprecision: number;
rprecision: number;
ltype: string;
rtype: string;
name: string;
}
export interface Offers { [market: string]: OffersMarket; }
export interface OffersMarket {
buys: Offer[] | null;
sells: Offer[] | null;
}
export interface OffersData {
direction: string;
currencyCode: string;
minAmount: number;
amount: number;
price: number;
date: number;
useMarketBasedPrice: boolean;
marketPriceMargin: number;
paymentMethod: string;
id: string;
currencyPair: string;
primaryMarketDirection: string;
priceDisplayString: string;
primaryMarketAmountDisplayString: string;
primaryMarketMinAmountDisplayString: string;
primaryMarketVolumeDisplayString: string;
primaryMarketMinVolumeDisplayString: string;
primaryMarketPrice: number;
primaryMarketAmount: number;
primaryMarketMinAmount: number;
primaryMarketVolume: number;
primaryMarketMinVolume: number;
}
export interface Offer {
offer_id: string;
offer_date: number;
direction: string;
min_amount: string;
amount: string;
price: string;
volume: string;
payment_method: string;
offer_fee_txid: any;
}
export interface Tickers { [market: string]: Ticker | null; }
export interface Ticker {
last: string;
high: string;
low: string;
volume_left: string;
volume_right: string;
buy: string | null;
sell: string | null;
}
export interface Trade {
market?: string;
price: string;
amount: string;
volume: string;
payment_method: string;
trade_id: string;
trade_date: number;
_market: Pair;
}
export interface TradesData {
currency: string;
direction: string;
tradePrice: number;
tradeAmount: number;
tradeDate: number;
paymentMethod: string;
offerDate: number;
useMarketBasedPrice: boolean;
marketPriceMargin: number;
offerAmount: number;
offerMinAmount: number;
offerId: string;
depositTxId?: string;
currencyPair: string;
primaryMarketDirection: string;
primaryMarketTradePrice: number;
primaryMarketTradeAmount: number;
primaryMarketTradeVolume: number;
_market: string;
_tradePriceStr: string;
_tradeAmountStr: string;
_tradeVolumeStr: string;
_offerAmountStr: string;
_tradePrice: number;
_tradeAmount: number;
_tradeVolume: number;
_offerAmount: number;
}
export interface MarketVolume {
period_start: number;
num_trades: number;
volume: string;
}
export interface MarketsApiError {
success: number;
error: string;
}
export type Interval = 'minute' | 'half_hour' | 'hour' | 'half_day' | 'day' | 'week' | 'month' | 'year' | 'auto';
export interface SummarizedIntervals { [market: string]: SummarizedInterval; }
export interface SummarizedInterval {
period_start: number;
open: number;
close: number;
high: number;
low: number;
avg: number;
volume_right: number;
volume_left: number;
time?: number;
}

View File

@@ -3,10 +3,15 @@ import { BisqRoutingModule } from './bisq.routing.module';
import { SharedModule } from '../shared/shared.module';
import { NgxBootstrapMultiselectModule } from 'ngx-bootrap-multiselect';
import { LightweightChartsComponent } from './lightweight-charts/lightweight-charts.component';
import { LightweightChartsAreaComponent } from './lightweight-charts-area/lightweight-charts-area.component';
import { BisqMarketComponent } from './bisq-market/bisq-market.component';
import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component';
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component';
import { BisqBlockComponent } from './bisq-block/bisq-block.component';
import { BisqDashboardComponent } from './bisq-dashboard/bisq-dashboard.component';
import { BisqMainDashboardComponent } from './bisq-main-dashboard/bisq-main-dashboard.component';
import { BisqIconComponent } from './bisq-icon/bisq-icon.component';
import { BisqTransactionDetailsComponent } from './bisq-transaction-details/bisq-transaction-details.component';
import { BisqTransfersComponent } from './bisq-transfers/bisq-transfers.component';
@@ -14,11 +19,11 @@ import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontaweso
import { faLeaf, faQuestion, faExclamationTriangle, faRocket, faRetweet, faFileAlt, faMoneyBill,
faEye, faEyeSlash, faLock, faLockOpen, faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
import { BisqBlocksComponent } from './bisq-blocks/bisq-blocks.component';
import { BisqExplorerComponent } from './bisq-explorer/bisq-explorer.component';
import { BisqApiService } from './bisq-api.service';
import { BisqAddressComponent } from './bisq-address/bisq-address.component';
import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
import { BsqAmountComponent } from './bsq-amount/bsq-amount.component';
import { BisqTradesComponent } from './bisq-trades/bisq-trades.component';
@NgModule({
declarations: [
@@ -30,10 +35,15 @@ import { BsqAmountComponent } from './bsq-amount/bsq-amount.component';
BisqTransactionDetailsComponent,
BisqTransfersComponent,
BisqBlocksComponent,
BisqExplorerComponent,
BisqAddressComponent,
BisqStatsComponent,
BsqAmountComponent,
LightweightChartsComponent,
LightweightChartsAreaComponent,
BisqDashboardComponent,
BisqMarketComponent,
BisqTradesComponent,
BisqMainDashboardComponent,
],
imports: [
BisqRoutingModule,

View File

@@ -5,55 +5,68 @@ import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions
import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component';
import { BisqBlockComponent } from './bisq-block/bisq-block.component';
import { BisqBlocksComponent } from './bisq-blocks/bisq-blocks.component';
import { BisqExplorerComponent } from './bisq-explorer/bisq-explorer.component';
import { BisqAddressComponent } from './bisq-address/bisq-address.component';
import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
import { ApiDocsComponent } from '../components/api-docs/api-docs.component';
import { BisqDashboardComponent } from './bisq-dashboard/bisq-dashboard.component';
import { BisqMarketComponent } from './bisq-market/bisq-market.component';
import { BisqMainDashboardComponent } from './bisq-main-dashboard/bisq-main-dashboard.component';
import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component';
const routes: Routes = [
{
path: '',
component: BisqExplorerComponent,
children: [
{
path: '',
component: BisqTransactionsComponent
},
{
path: 'tx/:id',
component: BisqTransactionComponent
},
{
path: 'blocks',
children: [],
component: BisqBlocksComponent
},
{
path: 'block/:id',
component: BisqBlockComponent,
},
{
path: 'address/:id',
component: BisqAddressComponent,
},
{
path: 'stats',
component: BisqStatsComponent,
},
{
path: 'about',
component: AboutComponent,
},
{
path: 'api',
component: ApiDocsComponent,
},
{
path: '**',
redirectTo: ''
}
]
}
{
path: '',
component: BisqMainDashboardComponent,
},
{
path: 'markets',
component: BisqDashboardComponent,
},
{
path: 'transactions',
component: BisqTransactionsComponent
},
{
path: 'market/:pair',
component: BisqMarketComponent,
},
{
path: 'tx/:id',
component: BisqTransactionComponent
},
{
path: 'blocks',
children: [],
component: BisqBlocksComponent
},
{
path: 'block/:id',
component: BisqBlockComponent,
},
{
path: 'address/:id',
component: BisqAddressComponent,
},
{
path: 'stats',
component: BisqStatsComponent,
},
{
path: 'about',
component: AboutComponent,
},
{
path: 'api',
component: ApiDocsComponent,
},
{
path: 'terms-of-service',
component: TermsOfServiceComponent
},
{
path: '**',
redirectTo: ''
}
];
@NgModule({

View File

@@ -2,5 +2,5 @@
<span [class.green-color]="green">{{ conversions.USD * bsq / 100 * (bsqPrice$ | async) / 100000000 | currency:'USD':'symbol':'1.2-2' }}</span>
</ng-container>
<ng-template #viewFiatVin>
{{ bsq / 100 | number : digitsInfo }} BSQ
{{ bsq / 100 | number : digitsInfo }} <span class="symbol">BSQ</span>
</ng-template>

View File

@@ -0,0 +1,25 @@
:host ::ng-deep .floating-tooltip-2 {
width: 160px;
height: 80px;
position: absolute;
display: none;
padding: 8px;
box-sizing: border-box;
font-size: 12px;
color:rgba(255, 255, 255, 1);
background-color: #131722;
text-align: left;
z-index: 1000;
top: 12px;
left: 12px;
pointer-events: none;
border-radius: 2px;
}
:host ::ng-deep .volumeText {
color: rgba(33, 150, 243, 0.7);
}
:host ::ng-deep .tradesText {
color: rgba(37, 177, 53, 1);
}

View File

@@ -0,0 +1,142 @@
import { createChart, CrosshairMode, isBusinessDay } from 'lightweight-charts';
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-lightweight-charts-area',
template: '<ng-component></ng-component>',
styleUrls: ['./lightweight-charts-area.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LightweightChartsAreaComponent implements OnInit, OnChanges, OnDestroy {
@Input() data: any;
@Input() lineData: any;
@Input() precision: number;
@Input() height = 500;
areaSeries: any;
volumeSeries: any;
chart: any;
lineSeries: any;
container: any;
width: number;
constructor(
private element: ElementRef,
) { }
ngOnInit() {
this.width = this.element.nativeElement.parentElement.offsetWidth;
this.container = document.createElement('div');
const chartholder = this.element.nativeElement.appendChild(this.container);
this.chart = createChart(chartholder, {
width: this.width,
height: this.height,
crosshair: {
mode: CrosshairMode.Normal,
},
layout: {
backgroundColor: '#000',
textColor: 'rgba(255, 255, 255, 0.8)',
},
grid: {
vertLines: {
color: 'rgba(255, 255, 255, 0.1)',
},
horzLines: {
color: 'rgba(255, 255, 255, 0.1)',
},
},
rightPriceScale: {
borderColor: 'rgba(255, 255, 255, 0.2)',
},
timeScale: {
borderColor: 'rgba(255, 255, 255, 0.2)',
},
});
this.lineSeries = this.chart.addLineSeries({
color: 'rgba(37, 177, 53, 1)',
lineColor: 'rgba(216, 27, 96, 1)',
lineWidth: 2,
});
this.areaSeries = this.chart.addAreaSeries({
topColor: 'rgba(33, 150, 243, 0.7)',
bottomColor: 'rgba(33, 150, 243, 0.1)',
lineColor: 'rgba(33, 150, 243, 0.1)',
lineWidth: 2,
});
const toolTip = document.createElement('div');
toolTip.className = 'floating-tooltip-2';
chartholder.appendChild(toolTip);
this.chart.subscribeCrosshairMove((param) => {
if (!param.time || param.point.x < 0 || param.point.x > this.width || param.point.y < 0 || param.point.y > this.height) {
toolTip.style.display = 'none';
return;
}
const dateStr = isBusinessDay(param.time)
? this.businessDayToString(param.time)
: new Date(param.time * 1000).toLocaleDateString();
toolTip.style.display = 'block';
const price = param.seriesPrices.get(this.areaSeries);
const line = param.seriesPrices.get(this.lineSeries);
const tradesText = $localize`:@@bisq-graph-trades:Trades`;
const volumeText = $localize`:@@bisq-graph-volume:Volume`;
toolTip.innerHTML = `<table>
<tr><td class="tradesText">${tradesText}:</td><td class="text-right tradesText">${Math.round(line * 100) / 100}</td></tr>
<tr><td class="volumeText">${volumeText}:<td class="text-right volumeText">${Math.round(price * 100) / 100} BTC</td></tr>
</table>
<div>${dateStr}</div>`;
const y = param.point.y;
const toolTipWidth = 100;
const toolTipHeight = 80;
const toolTipMargin = 15;
let left = param.point.x + toolTipMargin;
if (left > this.width - toolTipWidth) {
left = param.point.x - toolTipMargin - toolTipWidth;
}
let top = y + toolTipMargin;
if (top > this.height - toolTipHeight) {
top = y - toolTipHeight - toolTipMargin;
}
toolTip.style.left = left + 'px';
toolTip.style.top = top + 'px';
});
this.updateData();
}
businessDayToString(businessDay) {
return businessDay.year + '-' + businessDay.month + '-' + businessDay.day;
}
ngOnChanges(changes: SimpleChanges) {
if (!changes.data || changes.data.isFirstChange()){
return;
}
this.updateData();
}
updateData() {
this.areaSeries.setData(this.data);
this.lineSeries.setData(this.lineData);
}
ngOnDestroy() {
this.chart.remove();
}
}

View File

@@ -0,0 +1,86 @@
import { createChart, CrosshairMode } from 'lightweight-charts';
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-lightweight-charts',
template: '<ng-component></ng-component>',
styleUrls: ['./lightweight-charts.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LightweightChartsComponent implements OnInit, OnChanges, OnDestroy {
@Input() data: any;
@Input() volumeData: any;
@Input() precision: number;
@Input() height = 500;
lineSeries: any;
volumeSeries: any;
chart: any;
constructor(
private element: ElementRef,
) { }
ngOnInit() {
this.chart = createChart(this.element.nativeElement, {
width: this.element.nativeElement.parentElement.offsetWidth,
height: this.height,
layout: {
backgroundColor: '#000000',
textColor: '#d1d4dc',
},
crosshair: {
mode: CrosshairMode.Normal,
},
grid: {
vertLines: {
visible: true,
color: 'rgba(42, 46, 57, 0.5)',
},
horzLines: {
color: 'rgba(42, 46, 57, 0.5)',
},
},
});
this.lineSeries = this.chart.addCandlestickSeries();
this.volumeSeries = this.chart.addHistogramSeries({
color: '#26a69a',
priceFormat: {
type: 'volume',
},
priceScaleId: '',
scaleMargins: {
top: 0.85,
bottom: 0,
},
});
this.updateData();
}
ngOnChanges(changes: SimpleChanges) {
if (!changes.data || changes.data.isFirstChange()){
return;
}
this.updateData();
}
ngOnDestroy() {
this.chart.remove();
}
updateData() {
this.lineSeries.setData(this.data);
this.volumeSeries.setData(this.volumeData);
this.lineSeries.applyOptions({
priceFormat: {
type: 'price',
precision: this.precision,
minMove: 0.0000001,
},
});
}
}

View File

@@ -1,408 +1,214 @@
<div class="container-xl">
<div class="text-center">
<br>
<img src="./resources/mempool-logo-bigger.png" height="62.5" width="250">
<br>
<div class="container-xl about-page">
<div class="text-small text-center offset-md-1">
<div class="intro">
<span style="margin-left: auto; margin-right: -20px; margin-bottom: -20px">&trade;</span>
<img class="logo" src="./resources/mempool-logo-bigger.png" />
<div class="version">
v{{ packetJsonVersion }} [{{ frontendGitCommitHash }}]
</div>
</div>
<br>
<div class="about-text">
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container> &trade;</h5>
<p i18n>Building a mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, without any advertising, altcoins, or third-party trackers.</p>
</div>
<h5 i18n="about.about-the-project">The Mempool Open Source Project</h5>
<div class="row row-cols-1">
<div class="col col-md-6 mx-auto">
<p i18n>An explorer and API developed and operated for the Bitcoin community, focusing on the emerging transaction fee market to help our transition into a multi-layer ecosystem, without ads, altcoins, or third-party trackers.</p>
</div>
<div class="social-icons">
<a target="_blank" href="https://github.com/mempool/mempool">
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="github" class="svg-inline--fa fa-github fa-w-16 fa-4x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path fill="currentColor" d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path></svg>
</a>
<a target="_blank" href="https://twitter.com/mempool">
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="twitter" class="svg-inline--fa fa-twitter fa-w-16 fa-4x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path></svg>
</a>
<a target="_blank" href="https://t.me/mempoolspace">
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="telegram-plane" class="svg-inline--fa fa-telegram-plane fa-w-14 fa-4x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M446.7 98.6l-67.6 318.8c-5.1 22.5-18.4 28.1-37.3 17.5l-103-75.9-49.7 47.8c-5.5 5.5-10.1 10.1-20.7 10.1l7.4-104.9 190.9-172.5c8.3-7.4-1.8-11.5-12.9-4.1L117.8 284 16.2 252.2c-22.1-6.9-22.5-22.1 4.6-32.7L418.2 66.4c18.4-6.9 34.5 4.1 28.5 32.2z"></path></svg>
</a>
<a target="_blank" href="https://keybase.io/team/mempool">
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="keybase" class="svg-inline--fa fa-keybase fa-w-14 fa-4x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M286.17 419a18 18 0 1 0 18 18 18 18 0 0 0-18-18zm111.92-147.6c-9.5-14.62-39.37-52.45-87.26-73.71q-9.1-4.06-18.38-7.27a78.43 78.43 0 0 0-47.88-104.13c-12.41-4.1-23.33-6-32.41-5.77-.6-2-1.89-11 9.4-35L198.66 32l-5.48 7.56c-8.69 12.06-16.92 23.55-24.34 34.89a51 51 0 0 0-8.29-1.25c-41.53-2.45-39-2.33-41.06-2.33-50.61 0-50.75 52.12-50.75 45.88l-2.36 36.68c-1.61 27 19.75 50.21 47.63 51.85l8.93.54a214 214 0 0 0-46.29 35.54C14 304.66 14 374 14 429.77v33.64l23.32-29.8a148.6 148.6 0 0 0 14.56 37.56c5.78 10.13 14.87 9.45 19.64 7.33 4.21-1.87 10-6.92 3.75-20.11a178.29 178.29 0 0 1-15.76-53.13l46.82-59.83-24.66 74.11c58.23-42.4 157.38-61.76 236.25-38.59 34.2 10.05 67.45.69 84.74-23.84.72-1 1.2-2.16 1.85-3.22a156.09 156.09 0 0 1 2.8 28.43c0 23.3-3.69 52.93-14.88 81.64-2.52 6.46 1.76 14.5 8.6 15.74 7.42 1.57 15.33-3.1 18.37-11.15C429 443 434 414 434 382.32c0-38.58-13-77.46-35.91-110.92zM142.37 128.58l-15.7-.93-1.39 21.79 13.13.78a93 93 0 0 0 .32 19.57l-22.38-1.34a12.28 12.28 0 0 1-11.76-12.79L107 119c1-12.17 13.87-11.27 13.26-11.32l29.11 1.73a144.35 144.35 0 0 0-7 19.17zm148.42 172.18a10.51 10.51 0 0 1-14.35-1.39l-9.68-11.49-34.42 27a8.09 8.09 0 0 1-11.13-1.08l-15.78-18.64a7.38 7.38 0 0 1 1.34-10.34l34.57-27.18-14.14-16.74-17.09 13.45a7.75 7.75 0 0 1-10.59-1s-3.72-4.42-3.8-4.53a7.38 7.38 0 0 1 1.37-10.34L214 225.19s-18.51-22-18.6-22.14a9.56 9.56 0 0 1 1.74-13.42 10.38 10.38 0 0 1 14.3 1.37l81.09 96.32a9.58 9.58 0 0 1-1.74 13.44zM187.44 419a18 18 0 1 0 18 18 18 18 0 0 0-18-18z"></path></svg>
</a>
</div>
<div class="enterprise-sponsor">
<h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
<div class="wrapper">
<a href="https://squarecrypto.org/" target="_blank" title="Square Crypto">
<img class="image" src="/resources/profile/sqcrypto.svg" />
<span>Square</span>
</a>
<a href="https://gemini.com/" target="_blank" title="Gemini">
<img class="image" src="/resources/profile/gemini.svg" />
<span>Gemini</span>
</a>
<a href="https://exodus.com/" target="_blank" title="Exodus">
<img class="image" src="/resources/profile/exodus.svg" />
<span>Exodus</span>
</a>
</div>
</div>
<a target="_blank" class="m-2 fw6 mb3 mt2 truncate black-80 f4 link" href="https://github.com/mempool/mempool">
<span class="dib v-mid">
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="github" class="svg-inline--fa fa-github fa-w-16 fa-4x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path fill="currentColor" d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path></svg>
</span>
</a>
<a target="_blank" class="m-2 fw6 mb3 mt2 truncate black-80 f4 link" href="https://twitter.com/mempool">
<span class="dib v-mid">
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="twitter" class="svg-inline--fa fa-twitter fa-w-16 fa-4x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path></svg>
</span>
</a>
<a target="_blank" class="m-2 fw6 mb3 mt2 truncate black-80 f4 link" href="https://t.me/mempoolspace">
<span class="dib v-mid">
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="telegram-plane" class="svg-inline--fa fa-telegram-plane fa-w-14 fa-4x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M446.7 98.6l-67.6 318.8c-5.1 22.5-18.4 28.1-37.3 17.5l-103-75.9-49.7 47.8c-5.5 5.5-10.1 10.1-20.7 10.1l7.4-104.9 190.9-172.5c8.3-7.4-1.8-11.5-12.9-4.1L117.8 284 16.2 252.2c-22.1-6.9-22.5-22.1 4.6-32.7L418.2 66.4c18.4-6.9 34.5 4.1 28.5 32.2z"></path></svg>
</span>
</a>
<a target="_blank" class="m-2 fw6 mb3 mt2 truncate black-80 f4 link" href="https://keybase.io/team/mempool">
<span class="dib v-mid">
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="keybase" class="svg-inline--fa fa-keybase fa-w-14 fa-4x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M286.17 419a18 18 0 1 0 18 18 18 18 0 0 0-18-18zm111.92-147.6c-9.5-14.62-39.37-52.45-87.26-73.71q-9.1-4.06-18.38-7.27a78.43 78.43 0 0 0-47.88-104.13c-12.41-4.1-23.33-6-32.41-5.77-.6-2-1.89-11 9.4-35L198.66 32l-5.48 7.56c-8.69 12.06-16.92 23.55-24.34 34.89a51 51 0 0 0-8.29-1.25c-41.53-2.45-39-2.33-41.06-2.33-50.61 0-50.75 52.12-50.75 45.88l-2.36 36.68c-1.61 27 19.75 50.21 47.63 51.85l8.93.54a214 214 0 0 0-46.29 35.54C14 304.66 14 374 14 429.77v33.64l23.32-29.8a148.6 148.6 0 0 0 14.56 37.56c5.78 10.13 14.87 9.45 19.64 7.33 4.21-1.87 10-6.92 3.75-20.11a178.29 178.29 0 0 1-15.76-53.13l46.82-59.83-24.66 74.11c58.23-42.4 157.38-61.76 236.25-38.59 34.2 10.05 67.45.69 84.74-23.84.72-1 1.2-2.16 1.85-3.22a156.09 156.09 0 0 1 2.8 28.43c0 23.3-3.69 52.93-14.88 81.64-2.52 6.46 1.76 14.5 8.6 15.74 7.42 1.57 15.33-3.1 18.37-11.15C429 443 434 414 434 382.32c0-38.58-13-77.46-35.91-110.92zM142.37 128.58l-15.7-.93-1.39 21.79 13.13.78a93 93 0 0 0 .32 19.57l-22.38-1.34a12.28 12.28 0 0 1-11.76-12.79L107 119c1-12.17 13.87-11.27 13.26-11.32l29.11 1.73a144.35 144.35 0 0 0-7 19.17zm148.42 172.18a10.51 10.51 0 0 1-14.35-1.39l-9.68-11.49-34.42 27a8.09 8.09 0 0 1-11.13-1.08l-15.78-18.64a7.38 7.38 0 0 1 1.34-10.34l34.57-27.18-14.14-16.74-17.09 13.45a7.75 7.75 0 0 1-10.59-1s-3.72-4.42-3.8-4.53a7.38 7.38 0 0 1 1.37-10.34L214 225.19s-18.51-22-18.6-22.14a9.56 9.56 0 0 1 1.74-13.42 10.38 10.38 0 0 1 14.3 1.37l81.09 96.32a9.58 9.58 0 0 1-1.74 13.44zM187.44 419a18 18 0 1 0 18 18 18 18 0 0 0-18-18z"></path></svg>
</span>
</a>
<br><br>
<br><br>
<h3 i18n="about.sponsors.enterprise">Enterprise Sponsors 🚀</h3>
<a href="https://squarecrypto.org/" target="_blank">
<div class="profile_photo enterprise_sponsor d-inline-block" title="Square Crypto">
<img class="profile_img" src="/resources/profile/sqcrypto.svg" />
Square
</div>
</a>
<a href="https://gemini.com/" target="_blank">
<div class="profile_photo enterprise_sponsor d-inline-block" title="Gemini">
<img class="profile_img" src="/resources/profile/gemini.svg" />
Gemini
</div>
</a>
<a href="https://exodus.com/" target="_blank">
<div class="profile_photo enterprise_sponsor d-inline-block" title="Exodus">
<img class="profile_img" src="/resources/profile/exodus.svg" />
Exodus
</div>
</a>
<br><br>
<br><br>
<div class="community-sponsor">
<h3 i18n="about.sponsors.withHeart">Community Sponsors ❤️</h3>
<div *ngIf="sponsors === null">
<br>
<div class="spinner-border text-light"></div>
</div>
<ng-template ngFor let-sponsor [ngForOf]="sponsors">
<a [href]="'https://twitter.com/' + sponsor.handle" target="_blank" rel="sponsored">
<div class="profile_photo community_sponsor d-inline-block" [title]="sponsor.handle">
<img class="profile_img" [src]="'/api/v1/donations/images/' + sponsor.handle" />
</div>
</a>
</ng-template>
<br><br>
<button type="button" class="btn btn-primary" (click)="donationStatus = 2" [hidden]="donationStatus !== 1" i18n="about.become-a-sponsor">Become a sponsor ❤️</button>
<p *ngIf="donationStatus === 2 && !sponsorsEnabled">
<ng-container i18n="about.navigate-to-sponsor">Navigate to <a href="https://mempool.space/about" target="_blank">https://mempool.space/about</a> to sponsor</ng-container>
</p>
<div style="max-width: 300px;" class="mx-auto" [hidden]="donationStatus !== 2 || !sponsorsEnabled">
<form [formGroup]="donationForm" (submit)="submitDonation()" class="form">
<div class="input-group mb-2">
<div class="input-group-prepend" style="width: 42px;">
<span class="input-group-text"></span>
</div>
<input formControlName="amount" class="form-control" type="number" min="0.001" step="1E-03">
</div>
<div class="input-group" *ngIf="donationForm.get('amount').value >= 0.01; else lowAmount">
<div class="input-group-prepend" style="width: 42px;">
<span class="input-group-text">@</span>
</div>
<input formControlName="handle" class="form-control" type="text" placeholder="Twitter handle (Optional)">
</div>
<div class="required" *ngIf="donationForm.get('amount').hasError('required')" i18n="about.sponsor.amount-required">Amount required</div>
<div class="required" *ngIf="donationForm.get('amount').hasError('min')" i18n="about.sponsor.minimum-amount">Minimum amount is 0.001 BTC</div>
<div class="input-group mt-4">
<button class="btn btn-primary mx-auto" type="submit" [disabled]="donationForm.invalid" i18n="about.sponsor.request-invoice">Request invoice</button>
</div>
</form>
</div>
<ng-template #lowAmount>
<div class="input-group mb-4 text-small">
<span i18n="about.sponsor.description">If you donate 0.01 BTC or more, your profile photo will be added to the list of sponsors above :)</span>
</div>
</ng-template>
<div *ngIf="donationStatus === 3" class="text-center">
<form [formGroup]="paymentForm">
<div class="btn-group btn-group-toggle mb-2" ngbRadioGroup formControlName="method">
<label ngbButtonLabel class="btn-primary">
<input ngbButton type="radio" value="chain"> <fa-icon [icon]="['fas', 'link']" [fixedWidth]="true" title="Onchain"></fa-icon>
</label>
<label ngbButtonLabel class="btn-primary" *ngIf="donationObj.addresses.BTC_LightningLike">
<input ngbButton type="radio" value="lightning"> <fa-icon [icon]="['fas', 'bolt']" [fixedWidth]="true" title="Lightning"></fa-icon>
</label>
<label ngbButtonLabel class="btn-primary" *ngIf="donationObj.addresses.LBTC">
<input ngbButton type="radio" value="lbtc"> <fa-icon [icon]="['fas', 'tint']" [fixedWidth]="true" title="Liquid Bitcoin"></fa-icon>
</label>
</div>
</form>
<ng-template [ngIf]="paymentForm.get('method').value === 'chain'">
<div class="qr-wrapper mt-2 mb-2">
<a [href]="bypassSecurityTrustUrl('bitcoin:' + donationObj.addresses.BTC + '?amount=' + donationObj.amount)" target="_blank">
<app-qrcode imageUrl="./resources/bitcoin-logo.png" [size]="200" [data]="'bitcoin:' + donationObj.addresses.BTC + '?amount=' + donationObj.amount"></app-qrcode>
<div class="wrapper">
<ng-container *ngIf="sponsors$ | async as sponsors; else loadingSponsors">
<ng-template ngFor let-sponsor [ngForOf]="sponsors">
<a [href]="'https://twitter.com/' + sponsor.handle" target="_blank" rel="sponsored" [title]="sponsor.handle">
<img class="image" [src]="'/api/v1/donations/images/' + sponsor.handle" />
</a>
</div>
<br>
<div class="input-group input-group-sm mb-3 mt-3 info-group mx-auto">
<input type="text" class="form-control" readonly [value]="donationObj.addresses.BTC">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="donationObj.addresses.BTC"></app-clipboard></button>
</div>
</div>
<p style="font-size: 12px;">{{ donationObj.amount }} BTC</p>
</ng-template>
<ng-template [ngIf]="paymentForm.get('method').value === 'lightning'">
<div class="qr-wrapper mt-2 mb-2">
<a [href]="bypassSecurityTrustUrl('lightning:' + donationObj.addresses.BTC_LightningLike)" target="_blank">
<app-qrcode imageUrl="./resources/bitcoin-logo.png" [size]="200" [data]="donationObj.addresses.BTC_LightningLike.toUpperCase()"></app-qrcode>
</a>
</div>
<br>
<div class="input-group input-group-sm mb-3 mt-3 info-group mx-auto">
<input type="text" class="form-control" readonly [value]="donationObj.addresses.BTC_LightningLike">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button"><app-clipboard [text]="donationObj.addresses.BTC_LightningLike"></app-clipboard></button>
</div>
</div>
<div class="input-group input-group-sm mb-3 mt-3 info-group mx-auto">
<input type="text" class="form-control" readonly value="036f7fad4938521ddc6fc87ab7d6c6a091cef23cad87564a1f55adb806c017575e@103.99.170.198:9735">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button"><app-clipboard [text]="'036f7fad4938521ddc6fc87ab7d6c6a091cef23cad87564a1f55adb806c017575e@103.99.170.198:9735'"></app-clipboard></button>
</div>
</div>
<p style="font-size: 10px;"></p>
<p style="font-size: 12px;">{{ donationObj.amount }} BTC</p>
</ng-template>
<ng-template [ngIf]="paymentForm.get('method').value === 'lbtc'">
<div class="qr-wrapper mt-2 mb-2">
<a [href]="bypassSecurityTrustUrl('liquidnetwork:' + donationObj.addresses.LBTC + '?amount=' + donationObj.amount + '&assetid=6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d')" target="_blank">
<app-qrcode imageUrl="./resources/liquid-bitcoin.png" [size]="200" [data]="'liquidnetwork:' + donationObj.addresses.LBTC + '?amount=' + donationObj.amount + '&assetid=6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d'"></app-qrcode>
</a>
</div>
<br>
<div class="input-group input-group-sm mb-3 mt-3 info-group mx-auto">
<input type="text" class="form-control" readonly [value]="donationObj.addresses.LBTC">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="donationObj.addresses.LBTC"></app-clipboard></button>
</div>
</div>
<p style="font-size: 12px;">{{ donationObj.amount }} BTC</p>
</ng-template>
<p i18n="about.sponsor.waiting-for-transaction">Waiting for transaction... </p>
<div class="spinner-border text-light"></div>
</ng-template>
</ng-container>
</div>
<div *ngIf="donationStatus === 4" class="text-center">
<h2><span i18n="about.sponsor.donation-confirmed">Donation confirmed!</span><br><span i18n="about.sponsor.thank-you">Thank you!</span></h2>
</div>
<br><br>
<br><br>
<button [hidden]="showNavigateToSponsor" type="button" class="btn btn-primary" (click)="sponsor()" i18n="about.become-a-sponsor">Become a sponsor ❤️</button>
<ng-container *ngIf="showNavigateToSponsor" i18n="about.navigate-to-sponsor">Navigate to <a href="https://mempool.space/sponsor" target="_blank">https://mempool.space/sponsor</a> to sponsor</ng-container>
</div>
<div class="community-integrations-sponsor">
<h3 i18n="about.integrations">Community Integrations</h3>
<a href="https://github.com/bisq-network/bisq" target="_blank">
<div class="profile_photo d-inline-block" title="Bisq">
<img class="profile_img" src="/resources/profile/bisq_network.png" />
Bisq
</div>
</a>
<a href="https://github.com/getumbrel/umbrel" target="_blank">
<div class="profile_photo d-inline-block" title="Umbrel">
<img class="profile_img" src="/resources/profile/umbrel.png" />
Umbrel
</div>
</a>
<a href="https://github.com/rootzoll/raspiblitz" target="_blank">
<div class="profile_photo d-inline-block" title="RaspiBlitz">
<img class="profile_img" src="/resources/profile/raspiblitz.jpg" />
RaspiBlitz
</div>
</a>
<a href="https://github.com/mynodebtc/mynode" target="_blank">
<div class="profile_photo d-inline-block" title="MyNode">
<img class="profile_img" src="/resources/profile/mynodebtc.jpg" />
MyNode
</div>
</a>
<a href="https://github.com/RoninDojo/RoninDojo" target="_blank">
<div class="profile_photo d-inline-block" title="RoninDojo">
<img class="profile_img" src="/resources/profile/ronindojo.png" />
RoninDojo
</div>
</a>
<a href="https://github.com/spesmilo/electrum" target="_blank">
<div class="profile_photo d-inline-block" title="Electrum Wallet">
<img class="profile_img" src="/resources/profile/electrum.jpg" />
Electrum
</div>
</a>
<a href="https://github.com/cryptoadvance/specter-desktop" target="_blank">
<div class="profile_photo d-inline-block" title="Specter Wallet">
<img class="profile_img" src="/resources/profile/specter.png" />
Specter
</div>
</a>
<a href="https://github.com/sparrowwallet/sparrow" target="_blank">
<div class="profile_photo d-inline-block" title="Sparrow Wallet">
<img class="profile_img" src="/resources/profile/sparrow.png" />
Sparrow
</div>
</a>
<a href="https://github.com/ACINQ/phoenix" target="_blank">
<div class="profile_photo d-inline-block" title="Phoenix Wallet by ACINQ">
<img class="profile_img" src="/resources/profile/phoenix.jpg" />
Phoenix
</div>
</a>
<a href="https://github.com/muun/apollo" target="_blank">
<div class="profile_photo d-inline-block" title="Muun Wallet">
<img class="profile_img" src="/resources/profile/muun.png" />
Muun
</div>
</a>
<a href="https://github.com/BlueWallet/BlueWallet" target="_blank">
<div class="profile_photo d-inline-block" title="BlueWallet">
<img class="profile_img" src="/resources/profile/bluewallet.png" />
BlueWallet
</div>
</a>
<a href="https://github.com/hsjoberg/blixt-wallet" target="_blank">
<div class="profile_photo d-inline-block" title="Blixt Wallet">
<img class="profile_img" src="/resources/profile/blixt.png" />
Blixt
</div>
</a>
<a href="https://github.com/Satpile/satpile" target="_blank">
<div class="profile_photo d-inline-block" title="Satpile Watch-Only Wallet">
<img class="profile_img" src="/resources/profile/satpile.jpg" />
Satpile
</div>
</a>
<a href="https://github.com/btcontract/lnwallet" target="_blank">
<div class="profile_photo d-inline-block" title="Bitcoin Lightning Wallet">
<img class="profile_img" src="/resources/profile/blw.png" />
BLW
</div>
</a>
<a href="https://github.com/pxsocs/warden" target="_blank">
<div class="profile_photo d-inline-block" title="WARden Portfolio">
<img class="profile_img" src="/resources/profile/warden.jpg" />
WARden
</div>
</a>
<br><br>
<br><br>
<h3 i18n="about.alliances">Community Alliances</h3>
<a href="https://liquid.net/">
<img src="/resources/profile/liquid.svg" height="52">
</a>
<a href="https://opencrypto.org/">
<img src="/resources/profile/copa.png" height="62" style="margin-left: 30px; margin-right: 30px; margin-top: 20px;">
</a>
<a href="https://bisq.network/">
<img src="/resources/profile/bisq.svg" height="62">
</a>
<br><br>
<br><br>
<h3 i18n="about.contributors">Project Contributors</h3>
<div *ngIf="contributors === null">
<br>
<div class="spinner-border text-light"></div>
</div>
<ng-template ngFor let-contributor [ngForOf]="contributors">
<a [href]="'https://github.com/' + contributor.name" target="_blank">
<div class="profile_photo project_contributor d-inline-block" [title]="contributor.name">
<img class="profile_img" [src]="'/api/v1/contributors/images/' + contributor.id" />
{{ contributor.name }}
</div>
<div class="wrapper">
<a href="https://github.com/bisq-network/bisq" target="_blank" title="Bisq">
<img class="image" src="/resources/profile/bisq_network.png" />
<span>Bisq</span>
</a>
<a href="https://github.com/getumbrel/umbrel" target="_blank" title="Umbrel">
<img class="image" src="/resources/profile/umbrel.png" />
<span>Umbrel</span>
</a>
<a href="https://github.com/rootzoll/raspiblitz" target="_blank" title="RaspiBlitz">
<img class="image" src="/resources/profile/raspiblitz.jpg" />
<span>RaspiBlitz</span>
</a>
<a href="https://github.com/mynodebtc/mynode" target="_blank" title="MyNode">
<img class="image" src="/resources/profile/mynodebtc.jpg" />
<span>MyNode</span>
</a>
<a href="https://github.com/RoninDojo/RoninDojo" target="_blank" title="RoninDojo">
<img class="image" src="/resources/profile/ronindojo.png" />
<span>RoninDojo</span>
</a>
<a href="https://github.com/spesmilo/electrum" target="_blank" title="Electrum Wallet">
<img class="image" src="/resources/profile/electrum.jpg" />
<span>Electrum</span>
</a>
<a href="https://github.com/cryptoadvance/specter-desktop" target="_blank" title="Specter Wallet">
<img class="image" src="/resources/profile/specter.png" />
<span>Specter</span>
</a>
<a href="https://github.com/sparrowwallet/sparrow" target="_blank" title="Sparrow Wallet">
<img class="image" src="/resources/profile/sparrow.png" />
<span>Sparrow</span>
</a>
<a href="https://github.com/ACINQ/phoenix" target="_blank" title="Phoenix Wallet by ACINQ">
<img class="image" src="/resources/profile/phoenix.jpg" />
<span>Phoenix</span>
</a>
<a href="https://github.com/muun/apollo" target="_blank" title="Muun Wallet">
<img class="image" src="/resources/profile/muun.png" />
<span>Muun</span>
</a>
<a href="https://github.com/BlueWallet/BlueWallet" target="_blank" title="BlueWallet">
<img class="image" src="/resources/profile/bluewallet.png" />
<span>BlueWallet</span>
</a>
<a href="https://github.com/hsjoberg/blixt-wallet" target="_blank" title="Blixt Wallet">
<img class="image" src="/resources/profile/blixt.png" />
<span>Blixt</span>
</a>
<a href="https://github.com/Satpile/satpile" target="_blank" title="Satpile Watch-Only Wallet">
<img class="image" src="/resources/profile/satpile.jpg" />
<span>Satpile</span>
</a>
<a href="https://github.com/btcontract/lnwallet" target="_blank" title="Bitcoin Lightning Wallet">
<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>
</ng-template>
<br><br>
<br><br>
<h3 i18n="about.maintainers">Project Maintainers</h3>
<a href="https://twitter.com/softsimon_" target="_blank">
<div class="profile_photo d-inline-block" title="softsimon">
<img class="profile_img" src="/resources/profile/softsimon.jpg" />
softsimon
</div>
</a>
<a href="https://twitter.com/wiz" target="_blank">
<div class="profile_photo d-inline-block" title="wiz">
<img class="profile_img" src="/resources/profile/wiz.png" />
wiz
</div>
</a>
<br><br>
<br>
<div class="col-md-8 offset-md-2 text-center">
<code>Copyright (c) 2019-2021</code><br>
<code>The Mempool Open Source Project</code><br>
</div>
<br>
<div class="col-md-8 offset-md-2 text-left" style="padding-left: 20px">
<code>This program is free software; you can redistribute it and/or modify it under the terms of (at your option) either:</code><br>
</div>
<br>
<div class="col-md-8 offset-md-2 text-left" style="padding-left: 40px">
<code> 1) the <a href="https://www.gnu.org/licenses/agpl-3.0-standalone.html">GNU Affero General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>; or</code><br>
<br>
<code> 2) the <a href="https://www.gnu.org/licenses/gpl-3.0-standalone.html">GNU General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>.</code><br>
</div>
<br>
<div class="col-md-8 offset-md-2 text-left" style="padding-left: 20px">
<code>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details.</code><br>
</div>
</div>
<br>
<div class="alliances">
<h3 i18n="about.alliances">Community Alliances</h3>
<div class="wrapper">
<a href="https://liquid.net/" title="Liquid Network">
<img class="liquid" src="/resources/profile/liquid.svg" />
</a>
<a href="https://opencrypto.org/" title="Coppa - Crypto Open Patent Alliance">
<img class="copa" src="/resources/profile/copa.png" />
</a>
<a href="https://bisq.network/" title="Bisq Network">
<img class="bisq" src="/resources/profile/bisq.svg" />
</a>
</div>
</div>
<div class="text-center">
<div class="contributors">
<h3 i18n="about.contributors">Project Contributors</h3>
<div class="wrapper">
<ng-container *ngIf="contributors$ | async as contributors; else loadingSponsors">
<ng-template ngFor let-contributor [ngForOf]="contributors">
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name">
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" />
<span>{{ contributor.name }}</span>
</a>
</ng-template>
</ng-container>
</div>
</div>
<div class="maintainers">
<h3 i18n="about.maintainers">Project Maintainers</h3>
<div class="wrapper">
<a href="https://twitter.com/softsimon_" target="_blank" title="softsimon">
<img class="image" src="/resources/profile/softsimon.jpg" />
<span>softsimon</span>
</a>
<a href="https://twitter.com/wiz" target="_blank" title="wiz">
<img class="image" src="/resources/profile/wiz.png" />
<span>wiz</span>
</a>
</div>
</div>
<div class="copyright">
<div class="title">
Copyright (c) 2019-2021<br />
The Mempool Open Source Project
</div>
<p>
This program is free software; you can redistribute it and/or modify it under the terms of (at your option) either:<br>
</p>
<ul>
<li>
1) the <a href="https://www.gnu.org/licenses/agpl-3.0-standalone.html">GNU Affero General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>; or<br>
</li>
<li>
2) the <a href="https://www.gnu.org/licenses/gpl-3.0-standalone.html">GNU General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>.<br>
</li>
</ul>
<p>
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details.<br>
</p>
<p>
mempool.space&trade;, The Mempool Open Source Project&trade;, and the Mempool block logo are trademarks of Mempool Space K.K. in Japan.
</p>
</div>
<div class="footer-links">
<a href="/3rdpartylicenses.txt">Third-party Licenses</a>
<br>
<br>
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
</div>
<br>
<div class="text-small text-center" *ngIf="officialMempoolSpace">
<div class="footer-version" *ngIf="officialMempoolSpace">
{{ (backendInfo$ | async)?.hostname }} (v{{ (backendInfo$ | async )?.version }}) [{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}]
</div>
</div>
<ng-template #loadingSponsors>
<br>
<div class="spinner-border text-light"></div>
</ng-template>

View File

@@ -1,50 +1,179 @@
.qr-wrapper {
background-color: #FFF;
padding: 10px;
display: inline-block;
padding-bottom: 5px;
}
.profile_photo {
width: 80px;
height: 80px;
background-size: 100%, 100%;
border-radius: 50%;
margin: 25px;
line-height: 32px;
}
.about-page {
text-align: center;
.profile_img {
width: 80px;
height: 80px;
border-radius: 50%;
border: 0;
}
.image {
width: 80px;
height: 80px;
background-size: 100%, 100%;
border-radius: 50%;
margin: 25px;
line-height: 32px;
}
.community_sponsor {
margin: 6px;
}
.intro {
margin: 25px auto 30px;
width: 250px;
display: flex;
flex-direction: column;
.logo {
height: 62.5px;
width: 250px;
margin: auto;
}
.version {
text-align: right;
font-size: 10px;
margin-top: -10px;
}
}
.enterprise_sponsor {
margin-left: 40px;
margin-right: 40px;
}
.about-text {
max-width: 550px;
margin: auto;
padding: 10px 15px 15px;
}
.project_contributor {
width: 130px;
margin: 25px auto;
}
.social-icons {
a {
margin: auto 10px;
}
}
.text-small {
font-size: 12px;
}
.alliances,
.enterprise-sponsor,
.community-integrations-sponsor,
.maintainers {
margin-top: 68px;
margin-bottom: 68px;
}
.info-group {
max-width: 400px;
}
.maintainers {
margin-bottom: 50px;
}
.community-sponsor {
display: flex;
flex-direction: column;
.wrapper {
margin: 20px auto;
}
.btn-primary {
max-width: 250px;
margin: auto;
height: 45px;
width: 100%;
}
}
.required {
color: #FF0000;
font-weight: bold;
}
.alliances {
margin-bottom: 100px;
a {
&:nth-child(3) {
position: relative;
top: 10px;
}
}
img {
display: inline-block;
margin: 15px auto;
height: 62px;
@media (min-width: 425px) {
margin: 15px 60px;
}
@media (min-width: 576px) {
margin: 15px 120px;
}
@media (min-width: 850px) {
margin: 50px 30px 0px;
}
}
.liquid {
top: 7px;
position: relative;
}
.copa {
height: auto;
top: 23px;
position: relative;
width: 300px;
}
.bisq {
top: 3px;
position: relative;
}
}
.enterprise-sponsor,
.contributors,
.community-sponsor,
.community-integrations-sponsor,
.maintainers {
.wrapper {
display: inline-block;
a {
display: inline-block;
&:hover {
text-decoration: none;
img {
box-shadow: 0px 0px 20px #1bd8f4;
}
}
img, span{
display: block;
transition: 150ms all;
}
img {
margin: 40px 29px 10px;
}
}
}
}
.community-sponsor .wrapper {
margin: 10px auto 20px;
a img {
margin: 6px;
}
}
.copyright {
text-align: left;
max-width: 620px;
padding: 0px 15px;
margin: auto;
line-height: 1.8;
font-size: 87.5%;
color: #e83e8c;
word-wrap: break-word;
font-family: SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;
ul {
list-style: none;
padding-left: 30px;
li {
margin-bottom: 20px;
margin-top: 20px;
}
}
.title {
text-align: center;
margin-bottom: 25px;
}
}
.footer-links {
display: flex;
flex-direction: column;
a {
display: inline-block;
margin: 15px auto 0px;
&:last-child {
margin: 20px auto 30px;
}
}
}
.footer-version {
font-size: 12px;
}
}

View File

@@ -1,42 +1,33 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { WebsocketService } from '../../services/websocket.service';
import { SeoService } from 'src/app/services/seo.service';
import { StateService } from 'src/app/services/state.service';
import { Observable, Subscription } from 'rxjs';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Observable } from 'rxjs';
import { ApiService } from 'src/app/services/api.service';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { delay, retryWhen, switchMap, tap } from 'rxjs/operators';
import { IBackendInfo } from 'src/app/interfaces/websocket.interface';
import { Router } from '@angular/router';
@Component({
selector: 'app-about',
templateUrl: './about.component.html',
styleUrls: ['./about.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AboutComponent implements OnInit, OnDestroy {
export class AboutComponent implements OnInit {
backendInfo$: Observable<IBackendInfo>;
donationForm: FormGroup;
paymentForm: FormGroup;
donationStatus = 1;
sponsors$: Observable<any>;
contributors$: Observable<any>;
donationObj: any;
sponsorsEnabled = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
frontendGitCommitHash = this.stateService.env.GIT_COMMIT_HASH.substr(0, 8);
frontendGitCommitHash = this.stateService.env.GIT_COMMIT_HASH;
packetJsonVersion = this.stateService.env.PACKAGE_JSON_VERSION;
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
sponsors = null;
contributors = null;
requestSubscription: Subscription | undefined;
showNavigateToSponsor = false;
constructor(
private websocketService: WebsocketService,
private seoService: SeoService,
private stateService: StateService,
private formBuilder: FormBuilder,
private apiService: ApiService,
private sanitizer: DomSanitizer,
private router: Router,
) { }
ngOnInit() {
@@ -44,59 +35,15 @@ export class AboutComponent implements OnInit, OnDestroy {
this.seoService.setTitle($localize`:@@004b222ff9ef9dd4771b777950ca1d0e4cd4348a:About`);
this.websocketService.want(['blocks']);
this.donationForm = this.formBuilder.group({
amount: [0.01, [Validators.min(0.001), Validators.required]],
handle: [''],
});
this.paymentForm = this.formBuilder.group({
'method': 'chain'
});
this.apiService.getDonation$()
.subscribe((sponsors) => {
this.sponsors = sponsors;
});
this.apiService.getContributor$()
.subscribe((contributors) => {
this.contributors = contributors;
});
this.sponsors$ = this.apiService.getDonation$();
this.contributors$ = this.apiService.getContributor$();
}
ngOnDestroy() {
if (this.requestSubscription) {
this.requestSubscription.unsubscribe();
sponsor() {
if (this.officialMempoolSpace) {
this.router.navigateByUrl('/sponsor');
} else {
this.showNavigateToSponsor = true;
}
}
submitDonation() {
if (this.donationForm.invalid) {
return;
}
this.requestSubscription = this.apiService.requestDonation$(
this.donationForm.get('amount').value,
this.donationForm.get('handle').value
)
.pipe(
tap((response) => {
this.donationObj = response;
this.donationStatus = 3;
}),
switchMap(() => this.apiService.checkDonation$(this.donationObj.id)
.pipe(
retryWhen((errors) => errors.pipe(delay(2000)))
)
)
).subscribe(() => {
this.donationStatus = 4;
if (this.donationForm.get('handle').value) {
this.sponsors.unshift({ handle: this.donationForm.get('handle').value });
}
});
}
bypassSecurityTrustUrl(text: string): SafeUrl {
return this.sanitizer.bypassSecurityTrustUrl(text);
}
}

View File

@@ -1,10 +1,12 @@
<div class="container-xl">
<h1 class="float-left" i18n="shared.address">Address</h1>
<a [routerLink]="['/address/' | relativeUrl, addressString]" style="line-height: 56px; margin-left: 10px;">
<span class="d-inline d-lg-none">{{ addressString | shortenString : 18 }}</span>
<span class="d-none d-lg-inline">{{ addressString }}</span>
</a>
<app-clipboard [text]="addressString"></app-clipboard>
<h1 i18n="shared.address">Address</h1>
<div class="tx-link">
<a [routerLink]="['/address/' | relativeUrl, addressString]" >
<span class="d-inline d-lg-none">{{ addressString | shortenString : 18 }}</span>
<span class="d-none d-lg-inline">{{ addressString }}</span>
</a>
<app-clipboard [text]="addressString"></app-clipboard>
</div>
<br>
<div class="clearfix"></div>
@@ -13,7 +15,7 @@
<div class="box">
<div class="row">
<div class="col">
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody>
<ng-template [ngIf]="!address.electrum">
@@ -28,13 +30,13 @@
</ng-template>
<tr>
<td i18n="address.balance">Balance</td>
<td *ngIf="address.chain_stats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="receieved - sent" [noFiat]="true"></app-amount> (<app-fiat [value]="receieved - sent"></app-fiat>)</td>
<td *ngIf="address.chain_stats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="receieved - sent" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="receieved - sent"></app-fiat></span></td>
</tr>
</tbody>
</table>
</div>
<div class="w-100 d-block d-md-none"></div>
<div class="col qrcode-col">
<div class="col-md qrcode-col">
<div class="qr-wrapper">
<app-qrcode [data]="address.address"></app-qrcode>
</div>
@@ -78,6 +80,11 @@
</ng-container>
</ng-template>
<ng-template [ngIf]="retryLoadmore">
<br>
<button type="button" class="btn btn-outline-info btn-sm" (click)="loadMore()"><fa-icon [icon]="['fas', 'redo-alt']" [fixedWidth]="true"></fa-icon></button>
</ng-template>
</div>
</ng-template>
@@ -114,10 +121,13 @@
<div class="text-center">
<span i18n="address.error.loading-address-data">Error loading address data.</span>
<br>
<i>{{ error.error }}</i>
<ng-template [ngIf]="error.status === 413 || error.status === 405">
<br><br>
Consider viewing this address on the official Mempool website instead:
<ng-template #displayServerError><i>{{ error.error }}</i></ng-template>
<ng-template [ngIf]="error.status === 413 || error.status === 405" [ngIfElse]="displayServerError">
<ng-container i18n="Electrum server limit exceeded error">
<i>The number of transactions on this address exceeds the Electrum server limit</i>
<br><br>
Consider viewing this address on the official Mempool website instead:
</ng-container>
<br>
<a href="https://mempool.space/address/{{ addressString }}" target="_blank">https://mempool.space/address/{{ addressString }}</a>
<br>

View File

@@ -3,21 +3,76 @@
padding: 10px;
padding-bottom: 5px;
display: inline-block;
margin-right: 25px;
}
@media (min-width: 576px) {
.qrcode-col {
text-align: right;
.qrcode-col {
margin: 20px auto 10px;
text-align: center;
@media (min-width: 992px){
margin: 0px auto 0px;
}
}
@media (max-width: 575.98px) {
.qrcode-col {
text-align: center;
}
.qrcode-col > div {
margin-top: 20px;
margin-right: 0px;
.fiat {
display: block;
@media (min-width: 992px){
display: inline-block;
margin-left: 10px;
}
}
.table {
tr td {
&:last-child {
text-align: right;
@media (min-width: 576px) {
text-align: left;
}
}
}
}
h1 {
margin: 0px;
padding: 0px;
@media (min-width: 576px) {
float: left;
margin-right: 10px;
}
}
.address-link {
line-height: 56px;
margin-left: 0px;
top: -2px;
position: relative;
@media (min-width: 768px) {
line-height: 69px;
}
}
.row{
flex-direction: column;
@media (min-width: 576px) {
flex-direction: row;
}
}
@media (max-width: 767.98px) {
.mobile-bottomcol {
margin-top: 15px;
}
.details-table td:first-child {
white-space: pre-wrap;
}
}
.tx-link {
display: block;
width: 100%;
top: 14px;
position: relative;
@media (min-width: 768px) {
top: 20px;
}
}

View File

@@ -23,6 +23,7 @@ export class AddressComponent implements OnInit, OnDestroy {
isLoadingAddress = true;
transactions: Transaction[];
isLoadingTransactions = true;
retryLoadmore = false;
error: any;
mainSubscription: Subscription;
addressLoadingStatus$: Observable<number>;
@@ -183,12 +184,17 @@ export class AddressComponent implements OnInit, OnDestroy {
return;
}
this.isLoadingTransactions = true;
this.retryLoadmore = false;
this.electrsApiService.getAddressTransactionsFromHash$(this.address.address, this.lastTransactionTxId)
.subscribe((transactions: Transaction[]) => {
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
this.loadedConfirmedTxCount += transactions.length;
this.transactions = this.transactions.concat(transactions);
this.isLoadingTransactions = false;
},
(error) => {
this.isLoadingTransactions = false;
this.retryLoadmore = true;
});
}

View File

@@ -1,5 +1,5 @@
<ng-container *ngIf="!noFiat && (viewFiat$ | async) && (conversions$ | async) as conversions; else viewFiatVin">
<span>{{ conversions.USD * (satoshis / 100000000) | currency:'USD':'symbol':'1.2-2' }}</span>
<span class="fiat">{{ conversions.USD * (satoshis / 100000000) | currency:'USD':'symbol':'1.2-2' }}</span>
</ng-container>
<ng-template #viewFiatVin>
<ng-template [ngIf]="network === 'liquid' && (satoshis === undefined || satoshis === null)" [ngIfElse]="default">
@@ -7,8 +7,8 @@
</ng-template>
<ng-template #default>
&lrm;{{ satoshis / 100000000 | number : digitsInfo }}
<ng-template [ngIf]="network === 'liquid'">L-</ng-template>
<span class="symbol"><ng-template [ngIf]="network === 'liquid'">L-</ng-template>
<ng-template [ngIf]="network === 'testnet'">t</ng-template>
<ng-template [ngIf]="network === 'signet'">s</ng-template>BTC
<ng-template [ngIf]="network === 'signet'">s</ng-template>BTC</span>
</ng-template>
</ng-template>

View File

@@ -1,3 +1,3 @@
.green-color {
color: #3bcc49;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -14,3 +14,59 @@ tr {
.nowrap {
white-space: nowrap;
}
li.nav-item {
width: 100%;
@media (min-width: 676px){
width: auto;
}
}
.nav-tabs .nav-link.active {
border-bottom: 1px solid #fff;
@media (min-width: 676px){
border-bottom: 1px solid #11131f;
}
}
.code-tab {
width: auto;
margin: 20px auto 10px;
li.nav-item {
width: auto;
}
}
.code {
.tab-content {
padding: 0px;
}
.nav-tabs .nav-link.active {
border-bottom: 1px solid #11131f;
}
.subtitle {
display: flex;
justify-content: space-between;
}
}
.description {
margin-top: 20px;
}
.title {
font-weight: bold;
color: #ffffff;
font-size: 1.25rem;
}
.subtitle {
font-weight: bold;
}
.divider {
width: 100%;
margin: 30px auto;
height: 1px;
background: #333;
}
.websocket {
padding: 15px;
}

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