Compare commits

..

569 Commits

Author SHA1 Message Date
wiz
bc13e9f9f7 Bump version number to v2.1.2 2021-02-18 19:24:00 +09:00
wiz
cc0d8cb2b9 Merge remote-tracking branch 'origin/master' into v2.1 2021-02-18 19:21:31 +09:00
softsimon
d50e25bed7 Create README.md 2021-02-18 15:40:59 +07:00
wiz
7f8329aa4d Update project name and description on About page (#350) 2021-02-18 15:33:59 +07:00
wiz
6619f92502 Merge pull request #349 from mempool/simon/mempool-space-logo
Updating mempool logo and adding new mempool space logo.
2021-02-18 16:33:00 +09:00
softsimon
edce0d118a Updating mempool logo and adding new mempool space logo. New frontend config OFFICIAL_MEMPOOL_SPACE to enable the latter.
fixes #336
2021-02-18 13:34:05 +07:00
pox
26ba41cb91 a bit of formatting for the docker readme (#348) 2021-02-18 11:11:31 +07:00
Czino
475baf5634 Fix style on block hover (#347)
* Fix style on block hover

* Fix style on block hover

Co-authored-by: Eric Lehmann <eric@machinas.com>
2021-02-18 03:05:39 +07:00
wiz
6866b12e84 Update hard-coded LN node URI for sponsors on About page 2021-02-17 17:16:32 +09:00
softsimon
2743b35ea2 Update from transifex. 2021-02-15 00:15:04 +07:00
softsimon
d39207e097 Adding Telegram group link on about page. 2021-02-14 23:23:03 +07:00
wiz
033d1451d0 Merge pull request #343 from mempool/simon/clear-protection-config
Make clear protection timeout configurable.
2021-02-15 00:26:47 +09:00
wiz
befb192651 Reduce backend heap size setting to 2G since cache memory usage was optimized (#345)
On powerful servers, nodejs automatically sets the limit at 4GB
```
% node -e 'console.log(`node heap limit = ${require("v8").getHeapStatistics().heap_size_limit / (1024 * 1024)} Mb`)'
node heap limit = 4144 Mb
```

On a Raspberry Pi with 8GB RAM, nodejs automatically sets the limit at 1GB
```
% node -e 'console.log(`node heap limit = ${require("v8").getHeapStatistics().heap_size_limit / (1024 * 1024)} Mb`)'
node heap limit = 1048 Mb
```

On a Raspberry Pi with 4GB RAM, nodejs automatically sets the limit at 740MB
```
% node -e 'console.log(`node heap limit = ${require("v8").getHeapStatistics().heap_size_limit / (1024 * 1024)} Mb`)'
node heap limit = 739.4694900512695 Mb
```

After testing with manually setting the limit to 768MB, mempool starts
up fine, but crashes when saving the cache when the mempool is quite
large (over 400MB with custom bitcoin.conf setting).

So it's probably safe to reduce the 4GB limit setting to 2GB for
all devices and and just use the automatically set values, now that the
backend's disk cache memory usage was recently optimized.

However, a new npm script for `npm run start-production` will be added
so we can keep our production mempool.space servers running with a very
large bitcoin.conf mempool
2021-02-14 22:25:29 +07:00
softsimon
7489d3360a Make clear protection timeout configurable.
fixes #335
2021-02-14 20:32:00 +07:00
wiz
2641ae0c8e Merge pull request #342 from mempool/simon/optimize-disk-cache
Optimized cache chunks. Default cache files to cache/ directory.
2021-02-14 22:11:33 +09:00
softsimon
32290d1e0d Apply suggestions from code review
Co-authored-by: wiz <j@wiz.biz>
2021-02-14 20:03:45 +07:00
softsimon
584ef87fc8 Optimized cache chunks. Default cache files to /cache directory.
fixes #341
2021-02-14 19:50:31 +07:00
softsimon
63b52b9d9b Use mempool clear protection feature also when in bitcoind mode.
fixes #335
2021-02-14 01:04:44 +07:00
wiz
5f6b7d94b1 Update URL for RoninRojo repo to their self-hosted GitLab 2021-02-12 16:20:57 +09:00
wiz
95a1c002eb Update README with instructions for various installation methods 2021-02-11 14:57:43 +09:00
wiz
52a1425498 Bump version number to v2.1.1 2021-02-10 22:58:57 +09:00
wiz
51816e9938 Merge remote-tracking branch 'origin/master' into v2.1 2021-02-10 22:58:45 +09:00
wiz
2085833720 Fix docker/README example docker-compose.xml to use latest tag 2021-02-10 22:56:42 +09:00
Bastien Guillaumat
20ff62779d Re-implement Docker workflow using GitHub Actions (#326)
* Recreate a docker dir with all docker files inside and add init.sh
* Add docker/README.md with example docker-compose.xml
* Add mysql vars for mempool-config.json
* Port can be set from env var in docker-compose.xml
* Update docker-compose.xml example to use latest tag
* Remove armv7 architecture from build workflow
* Remove master git-hash file
* Remove useless ':' in sed
2021-02-10 22:51:01 +09:00
softsimon
181cb8e03f Removing lightning htlc matching since it has false positives.
refs #324
2021-02-10 19:55:11 +07:00
wiz
cef6bc8345 Enable our GitHub Sponsors program "Sponsor" button 2021-02-10 21:30:16 +09:00
Devin Bileck
a67d82ea94 Fix BSQ token market cap calculation (#332)
Fixes #331
2021-02-10 19:05:16 +07:00
softsimon
89efd410fb Fix for hard coded sponsor url.
refs #319
2021-02-07 03:00:02 +07:00
softsimon
eb0c20dd92 Removing sponsors code.
Support new sponsor confirmation polling.
fixes #319
2021-02-07 02:20:07 +07:00
softsimon
c733497e52 Adding v2.1 screen shot. 2021-02-04 17:58:23 +07:00
wiz
378a56b2c7 Set production electrs scripts to store db in /electrs 2021-02-04 15:53:08 +09:00
wiz
cc3d7be5c3 Set version number for v2.1.0 release 2021-02-04 13:08:15 +09:00
wiz
8e42d573ee Modify production electrs scripts to use relative paths from homedir 2021-02-04 12:17:28 +09:00
wiz
a7adf3a345 Bump version number to v2.2-SNAPSHOT 2021-02-04 12:16:47 +09:00
softsimon
ccdf41c5b6 Don't display multisig tag on lightning/liquid contracts.
refs #324
2021-02-03 19:03:59 +07:00
softsimon
0c979e9440 Only lowercase bech32 addresses in address prefix search.
fixes #327
2021-02-03 17:13:29 +07:00
wiz
de035dc1b5 Merge pull request #325 from bguillaumat/docker-error-backend
Fix missing mempool-config due to gitignore
2021-02-03 18:07:25 +09:00
softsimon
18d08ce4bf Updating from transifex. 2021-02-03 15:57:51 +07:00
Bastien
35f5efaa2e Fix missing mempool-config due to gitignore 2021-02-03 09:52:04 +01:00
softsimon
96b3ca6a0b Updating from transifex. 2021-02-03 15:30:38 +07:00
wiz
4e4f655be4 Rename repos on DockerHub to mempool/frontend and mempool/backend 2021-02-03 17:07:12 +09:00
wiz
985df53af6 Remove mempool-mysql Dockerfile target 2021-02-03 16:41:28 +09:00
wiz
ace480e3c7 Fix name and target tag of GitHub Actions workflow 2021-02-03 16:37:11 +09:00
wiz
e670172979 Disable building mempool-mysql Dockerfile from GitHub Action workflow 2021-02-03 16:30:42 +09:00
wiz
7ddbf5d3df Disable using docker buildkit in GitHub Action workflow 2021-02-03 16:28:37 +09:00
wiz
264bca9c56 Fix space indentation in .github/workflows/on-tag.yml 2021-02-03 16:18:00 +09:00
wiz
b53d364877 Add new GitHub action workflow on-tag to build and push to DockerHub 2021-02-03 16:08:59 +09:00
softsimon
ed17203a5f bitcoind: Parse witness scripts from P2SH transactions.
fixes #323
2021-02-03 13:11:14 +07:00
softsimon
999163d7ad Type fix for contract parsing.
refs #324
2021-02-03 12:51:30 +07:00
fiatjaf
276117fba9 better identification of lightning outputs. (#320)
* better identification of lightning outputs.
* identify liquid pegouts and emergency pegouts.
fixes #324
2021-02-03 12:40:31 +07:00
wiz
448cb8e264 Merge pull request #322 from bguillaumat/update-dockerization
Update dockerization
2021-02-03 13:03:44 +09:00
Bastien
6782229a3d Move docker files in subdir 2021-02-03 02:12:15 +01:00
Bastien
543fe8c735 Set all dir to be dockerized 2021-02-02 23:45:54 +01:00
Bastien
f3e97e2e2d Default dir should be the same than config-mempool.json 2021-02-02 19:25:33 +01:00
wiz
03179e34fb Merge pull request #317 from bguillaumat/add-cache-dir
Add Dockerization of Mempool v2 for Umbrel App Store
2021-02-03 03:21:22 +09:00
Bastien
a36b5c660d Fix ENV_VAR issues 2021-02-02 19:20:05 +01:00
Bastien
2d872bda47 Update package.json && Fix start.sh script 2021-02-02 18:01:23 +01:00
Bastien
91d4017aa6 Update ENV_VARIABLES in scripts && Fix some errors 2021-02-02 17:54:01 +01:00
Bastien Guillaumat
6efe055003 Apply Wiz suggestions
Co-authored-by: wiz <j@wiz.biz>
2021-02-02 16:34:21 +01:00
softsimon
f39d90547e Display P2PK inputs as "P2PK" instead of empty string.
fixes #290
2021-02-02 01:32:15 +07:00
Bastien
c26737ffd6 Update build.sh 2021-02-01 18:17:50 +01:00
softsimon
09f5f552bf Don't display fee rating when block medianFee is empty to fix "overpaid by infinity".
fixes #288
2021-02-02 00:10:57 +07:00
Bastien
feb5e96323 Add docker dir to build mempool containers 2021-02-01 17:25:08 +01:00
Bastien
f7ff6336f2 Add CACHE_DIR var 2021-02-01 15:54:27 +01:00
softsimon
2a5a4ddac0 Hide address received/sent from address page when using electrum API.
fixes #294
2021-02-01 04:52:24 +07:00
softsimon
fd869c732d Detect network from url also when languages are being used.
fixes #287
2021-02-01 04:17:34 +07:00
wiz
d3646aa45e Fix production nginx.conf and backend config to always use unique ports 2021-01-31 01:16:32 +09:00
softsimon
f3c18b152a Moved sponsors cache creation until after diskcache is done.
refs #315
2021-01-30 22:12:22 +07:00
wiz
6a9c4d82ec Rename production syslog notifications channel 2021-01-30 23:40:46 +09:00
softsimon
35521f4871 Refactored when statistics cache from database is created.
refs #315
2021-01-30 21:40:04 +07:00
softsimon
9b9c605cbe Upgrading mysql client lib. 2021-01-30 21:39:23 +07:00
softsimon
21d32dec41 Wait for database connection established before continuing startup.
refs #315
2021-01-30 21:09:30 +07:00
wiz
54a276439d Increase Bisq block height divergence warning to 10 blocks for Tor issues 2021-01-30 23:02:20 +09:00
wiz
42ad068dd2 Configure production backends and nginx for high availability electrs 2021-01-30 23:01:01 +09:00
softsimon
c5c2222b8c Increased database query timeouts
fixes #315
2021-01-30 19:20:52 +07:00
softsimon
9d317082e1 Replacing localhost with 127.0.0.1 2021-01-30 18:43:58 +07:00
wiz
edf8f1fc8a Update production upgrade script notification command 2021-01-30 20:31:47 +09:00
wiz
ad52e7fe7e Update production syslog scripts 2021-01-30 20:09:47 +09:00
softsimon
c94f004425 Adding missing default timeout on all axios requests.
fixes #314
2021-01-30 16:25:22 +07:00
softsimon
371433b2da Fix for fee estimate not being correct when block is almost full with more projected blocks in queue.
fixes #278
2021-01-30 01:34:40 +07:00
softsimon
3256198ab0 Credit Italian translator. 2021-01-29 16:58:04 +07:00
softsimon
da533097d9 Replacing weight with vsize in transactions api to match esplora. 2021-01-28 18:24:02 +07:00
softsimon
47dd1f2d0b Adding Italian language. Updating nginx conf with some missing languages. 2021-01-27 20:24:32 +07:00
softsimon
5f1f06fecf Don't save disk cache on exit. Handle corrupted mempool disk cache.
fixes #304
2021-01-27 01:49:11 +07:00
softsimon
b08225dab5 Readding deleted blocks api endpoint. 2021-01-27 00:33:16 +07:00
softsimon
9d02ab1eb5 Center align offline/reconnection badge on the logo.
fixes #302
2021-01-25 01:29:58 +07:00
softsimon
e86b7c7258 Adding missing basic API endpoints when in bitcoind/electrum mode.
fixes #291
2021-01-25 01:09:42 +07:00
softsimon
c9e175a0cc Always fetch at least the coinbase transaction of blocks, and throw error if failed. 2021-01-24 23:56:51 +07:00
softsimon
cca95bbd66 Increasing clear protection period to 10 minutes. 2021-01-24 19:33:05 +07:00
softsimon
86902d6f33 Updated from transifex. 2021-01-24 18:32:46 +07:00
softsimon
e214eedf23 Center aligning fee box text. 2021-01-24 17:26:14 +07:00
softsimon
39613cc2a2 Updated from transifex. 2021-01-24 14:08:06 +07:00
softsimon
dad122625f Adjusting h5 header font-size. 2021-01-24 13:59:39 +07:00
softsimon
f049e3abc4 Reducing size of dashboard header titles to reduce overflow depending on language. 2021-01-24 11:53:42 +07:00
softsimon
bf28dc1eea Credit hungarian translator. 2021-01-24 11:43:20 +07:00
softsimon
4d47388e25 Updating translations from transifex. 2021-01-24 04:21:07 +07:00
softsimon
6507b5e003 Adding hungarian language. 2021-01-24 04:19:50 +07:00
softsimon
47a449e1d9 Disable support for Electrum TX lookups (require -txindex). 2021-01-24 04:15:06 +07:00
softsimon
5b268794af Correctly error handle getTransaction and set 404 status when not found. 2021-01-24 02:51:22 +07:00
softsimon
fb41f58f7c Corrected Bisq stats calculation.
fixes #280
2021-01-23 19:26:05 +07:00
softsimon
e82c89a985 Only save cache on new block when in sync. 2021-01-22 23:20:39 +07:00
wiz
494119d119 Update translated strings from Transifex 2021-01-21 12:39:53 +09:00
softsimon
9900f4da80 Set backend to out of sync after connection retry interval has exceeded. 2021-01-20 17:16:43 +07:00
softsimon
a158794e2c Bitcoind: Lookup block transactions as long as in sync.
refs #288
2021-01-20 17:15:11 +07:00
softsimon
1a04b088fb Fix: Load mempool cache before checking DB connectivity. 2021-01-20 01:34:21 +07:00
softsimon
17b1325b3f Fix for case-insensitive address prefix search.
fixes #301
2021-01-19 13:01:47 +07:00
wiz
0336c6256a Don't create empty cache.json on startup anymore 2021-01-18 12:20:55 +09:00
softsimon
642e54b057 Adding missing space. 2021-01-16 17:34:58 +07:00
softsimon
23323be24a Updated i18n 2021-01-16 14:56:19 +07:00
softsimon
011f35ec94 Display a red RBF button when RBF is not enabled.
fixes #279
2021-01-16 00:24:23 +07:00
softsimon
9751af096d Adding common styling for dropdown select.
fixes #293
2021-01-16 00:10:23 +07:00
softsimon
1e81355e7d Increased disk cache chunks amount to 10 to fix json string length error. 2021-01-15 23:26:32 +07:00
softsimon
eff4d2c8cd Optionally support second CoreRPC instance as source for the minimum fee. 2021-01-15 22:23:32 +07:00
softsimon
d49c347413 Minimum fee / Purging UX updates. 2021-01-15 21:21:53 +07:00
softsimon
8aa3379ba5 Reduce displayed address length on mobile. 2021-01-13 02:10:04 +07:00
softsimon
ec845a6ac2 Renaming minimum fee to rejecting.
Adding progressbar to mempool size.
refs #171
2021-01-12 20:45:41 +07:00
wiz
46b7e6961e Merge pull request #289 from timlucmiptev/patch-1
README: update mariadb instructions
2021-01-12 20:49:35 +09:00
wiz
fc709058c1 Fix mariadb SQL command to GRANT ALL PRIVILEGES 2021-01-12 20:49:18 +09:00
wiz
627e8e5e9a Possible fix for occasional inability to change language to English
Fixes #275
2021-01-12 18:55:57 +09:00
softsimon
2b55ee1e07 Adding CoreRPC "Minimum fee" to the dashboard
fixes #171
2021-01-12 16:42:07 +07:00
wiz
695da602b8 Bump version string on about page to 2.1-SNAPSHOT 2021-01-12 18:10:48 +09:00
~timluc-miptev
3e09755c47 Update README.md 2021-01-11 20:44:45 +02:00
softsimon
4694a31f55 Fix memory leak by not stacking data in the scan accumulator.
Also handle going from zero to initial graph data on dashboard.
fixes #273
2021-01-12 00:15:52 +07:00
wiz
ab8cb033e6 Update backend/package-lock.json for axios security vulnerability 2021-01-11 23:27:27 +09:00
wiz
beb99bcfc6 Update README instructions and nginx.conf for v2.0 release 2021-01-11 23:14:10 +09:00
wiz
a05af48059 Add missing production/mempool-frontend-config.json 2021-01-11 23:14:08 +09:00
softsimon
f462cb27c6 Removing the term ELECTRS from ELECTRS_ITEMS_PER_PAGE setting. 2021-01-11 21:11:09 +07:00
softsimon
103740ec45 Setting default frontend config ELECTRS_ITEMS_PER_PAGE to 10 to adapt to CORE_RPC backends. 2021-01-11 21:06:47 +07:00
wiz
cc166cab75 Fix resource download path in frontend/sync-assets.js 2021-01-11 22:11:29 +09:00
wiz
29e0c337fe Fix pathname of frontend build output folder in npm run sync-assets 2021-01-11 22:05:39 +09:00
wiz
217666f455 Copy /en-US/resources files into top-level /resources/ after frontend build 2021-01-11 21:46:16 +09:00
softsimon
579ccaf21d Adding missing progressbar styling. 2021-01-11 17:20:52 +07:00
softsimon
4f1049bace npm audit fix 2021-01-11 16:04:16 +07:00
wiz
c31d4e35f6 Merge pull request #281 from mempool/simon/bitcoind
Bitcoind and Electrum Server backend support
2021-01-11 17:46:25 +09:00
wiz
3e2a49c08e Update production/mempool-config.*.json files for new backend config 2021-01-11 17:42:35 +09:00
softsimon
0a0e7fad3a Update backend/package.json
Co-authored-by: wiz <j@wiz.biz>
2021-01-11 15:23:59 +07:00
softsimon
e76ee93bbb Config updates. Renamed BITCOIND to CORE_RPC. 2021-01-11 14:53:18 +07:00
softsimon
27d2f5bd5a Merge branch 'master' into simon/bitcoind
* master:
  Update translated strings from Transifex
2021-01-11 14:42:38 +07:00
softsimon
20406fa522 Moved method $validateAddress 2021-01-11 14:30:58 +07:00
softsimon
905ddbb363 Bitcoind: Use mempool as address index when doing address prefix search. 2021-01-11 01:51:57 +07:00
softsimon
38d534caee Return not implemented error instead of empty array. 2021-01-11 00:26:36 +07:00
softsimon
e36646ac7c Adding missing error output on /blocks page. 2021-01-10 22:41:52 +07:00
softsimon
9689ccf2ac Bitcoind: Display the suggestion message on address page when address lookups is not implemented. 2021-01-10 22:24:36 +07:00
softsimon
c4d1fad853 Cache electrum address history fetch a couple of seconds to prevent double requests on address page load. 2021-01-10 19:58:55 +07:00
softsimon
9a23d2c6b0 Electrum: Sort address transactions correctly by confirmed and unconfirmed. 2021-01-10 17:40:05 +07:00
softsimon
065c21da1f Bitcoind: Push full transactions to address page and RBF mode. 2021-01-10 17:38:59 +07:00
softsimon
6a58717694 Loading progressbar for loading address, block transactions and blocks 2021-01-08 21:44:36 +07:00
wiz
b4e61634bc Update translated strings from Transifex 2021-01-08 00:23:53 +09:00
softsimon
b91516a1c1 Push new conversion rate updates to the clients. 2021-01-06 23:31:33 +07:00
softsimon
dc63fd9428 Config file updates. electrs -> esplora 2021-01-06 22:49:28 +07:00
softsimon
29dd6e5d8d Never run statistics when mempool not in sync. 2021-01-06 03:09:31 +07:00
softsimon
9e1ef1b747 Adding bitcoinJS-lib and parse P2SH and P2WSH scripts. 2021-01-06 01:36:28 +07:00
softsimon
632c243b34 Loading indicator service indicating mempool sync status. 2021-01-05 18:57:06 +07:00
softsimon
f46728080d Inform client when Electrum server limit exceeded 2021-01-05 17:30:53 +07:00
softsimon
1a6c2e79e6 Electrum protocol and tx lookup setting. 2021-01-05 03:06:57 +07:00
softsimon
7729ad8b79 Convert sighash to match esplora. 2021-01-03 22:02:10 +07:00
softsimon
a25125091d Hack to make it possible to load the Coinbase transaction from Bitcoin Core. 2021-01-02 04:40:10 +07:00
softsimon
89b4de2484 Fixed addressTransactions with pagination in electrum API. 2020-12-30 02:27:34 +07:00
softsimon
5390629e41 Improved block fetching performance. 2020-12-30 01:47:07 +07:00
softsimon
62c78f5b08 Adding getBlocks support. 2020-12-29 20:41:16 +07:00
softsimon
ae87694bc3 Use @mempool/electrum-client lib as Electrum Client lib. 2020-12-29 14:14:34 +07:00
softsimon
a3644e23a7 Switching Electrum Client lib supporting auto reconnect. 2020-12-29 00:41:02 +07:00
softsimon
3c0fa71a10 Updates for general transaction and block fetching. 2020-12-28 20:17:32 +07:00
softsimon
bb28a56622 Big refactor of multiple backends handling. 2020-12-28 04:47:22 +07:00
softsimon
8d0db12abe Merge branch 'master' into simon/bitcoind
* master: (22 commits)
  Update translated strings from Transifex
  Rewrite server.ts again to properly add i18n support into SSR mode
  Rewrite server.ts to fix i18n and implement proper HTTP 404 responses
  Update mempool-frontend-config.sample.json for NGINX_HOSTNAME vars
  Fixes after merging master into AU branch, add nginx.conf for SSR
  Adjust build scripts for new AU frontend build output in /browser/
  Always set a value for the locale selector, for /liquid/ etc.
  Correcting merge conflict.
  Route all Angular Universal requests to nginx, remove simon's hacks
  Hack to fix the urls.
  Fix for non matching request URLs.
  Adding ServerTransferStateModule for AU with custom http interceptor for cache transfer.
  removing /api again
  New frontend config for webserver url to fix static asset httpGet. Removed "absolute" from config names.
  Remove ssr prepended url from asset service.
  Don't append /api to electrs backend path for SSR
  Update absolute url config variables.
  Frontend config support for AU. New absolute server url settings. refs #104
  Transaction, address and block page now works.
  Update `og:title` meta tag when changing page title
  ...
2020-12-24 10:38:14 +07:00
wiz
8b1768f745 Update translated strings from Transifex 2020-12-24 04:16:00 +09:00
wiz
8f3db3690c Merge pull request #187 from mempool/simon/angular-universal
Angular Universal Milestone 1
2020-12-24 04:13:17 +09:00
wiz
87fbdc063b Rewrite server.ts again to properly add i18n support into SSR mode 2020-12-23 18:04:30 +09:00
wiz
b261243f85 Rewrite server.ts to fix i18n and implement proper HTTP 404 responses 2020-12-23 13:26:11 +09:00
wiz
2385ec7cde Update mempool-frontend-config.sample.json for NGINX_HOSTNAME vars 2020-12-22 13:11:31 +09:00
wiz
6491d4576a Fixes after merging master into AU branch, add nginx.conf for SSR 2020-12-22 11:31:49 +09:00
softsimon
f84b9e6582 Address page mostly working. 2020-12-22 06:04:31 +07:00
wiz
51ed2a1f31 Merge remote-tracking branch 'origin/master' into simon/angular-universal 2020-12-22 07:17:40 +09:00
wiz
0d50bbdc0a Adjust build scripts for new AU frontend build output in /browser/ 2020-12-22 07:09:45 +09:00
softsimon
df63117088 Merge pull request #274 from mempool/wiz/fix-locale-selector-default-value
Always set a value for the locale selector, for /liquid/ etc.
2020-12-22 05:06:24 +07:00
wiz
de50816990 Always set a value for the locale selector, for /liquid/ etc. 2020-12-22 06:41:56 +09:00
wiz
ff0d1a7589 Merge remote-tracking branch 'origin/master' into simon/angular-universal 2020-12-22 06:14:12 +09:00
softsimon
ecc0f316cc Refactored transaction handling. 2020-12-21 23:08:34 +07:00
softsimon
5dbf6789a7 Basic bitcoind/romanz-electrum support to sync the mempool and blocks. 2020-12-20 22:36:36 +07:00
wiz
5a4a976d55 Update translated strings from Transifex 2020-12-17 12:41:18 +09:00
wiz
1f5af33f14 Add missing websocket subscription for 'blocks' in status-view component 2020-12-15 03:48:07 +09:00
wiz
8f3901b6d4 Update translation strings from Transifex 2020-12-15 00:15:14 +09:00
wiz
6dc10f438a Update translation strings from Transifex 2020-12-14 04:36:05 +09:00
wiz
006e942109 Update translation strings from Transifex 2020-12-13 17:59:54 +09:00
wiz
71c405d8bc Update from Transifex 2020-12-13 01:33:42 +09:00
wiz
73bb712311 Fix formatting of frontend/README.md 2020-12-11 03:13:47 +09:00
wiz
e04c4cfa38 Add list of translators to frontend/README.md 2020-12-11 03:02:58 +09:00
softsimon
cd9f8cbf93 Updating from transifex. 2020-12-10 23:38:50 +07:00
wiz
4b1ac88a5c Remove duplicate locale for 'ka' in app.constants.ts 2020-12-10 23:14:29 +09:00
softsimon
1309bfc56e Merge pull request #268 from TechMiX/fixRTLproblems
add rtl-layout class to base component
2020-12-10 12:16:01 +07:00
softsimon
ecf49c4365 Updated from transifex. 2020-12-10 11:53:16 +07:00
softsimon
d0e0ab7c24 i18n: Enabling Vietnamese 2020-12-10 11:51:27 +07:00
TechMiX
95c9b21149 add left-to-right mark character to blockchain and mempool block components size 2020-12-09 21:47:27 +01:00
TechMiX
306197c6e9 fix blockchain scrolling on mobile mode when RTL layout 2020-12-09 21:46:13 +01:00
softsimon
56556b6c52 Updated strings from transifex. 2020-12-09 23:55:17 +07:00
softsimon
d8e1023848 i18n: Enabling Finnish 2020-12-09 23:20:37 +07:00
softsimon
c9f485a775 i18n: Disabling vietnamese again 2020-12-09 23:20:10 +07:00
TechMiX
e6be450aaa add left-to-right mark character to amount component 2020-12-09 02:33:20 +01:00
TechMiX
c1bcf49f37 reverse all text-aligns in rtl-layout 2020-12-09 02:23:53 +01:00
TechMiX
c4734357c4 add text-align for Details title + swap text-left class with style so that it always remains aligned to left 2020-12-09 02:22:16 +01:00
TechMiX
e0adc505db add rtl-layout class to base component 2020-12-09 01:57:18 +01:00
softsimon
260735665b Status view live data now works again... 2020-12-09 01:37:14 +07:00
softsimon
5b8bcd5b7d i18n: Fixed stacked mempool block issue and enabled vietnamese. 2020-12-09 01:31:49 +07:00
softsimon
0a6f6a14b6 Pulled new strings from transifex. 2020-12-09 01:17:22 +07:00
softsimon
8e5efc47cd i18n: reuse vB/s string 2020-12-08 18:40:00 +07:00
softsimon
eb9aaac0aa i18n: Fixed translations for the footer component. Merged some duplicates. 2020-12-08 18:24:02 +07:00
softsimon
2c1514b6cf Merge pull request #266 from mempool/wiz/enable-korean-locale
Enable 'ko' locale for Korean
2020-12-08 14:00:53 +07:00
wiz
06a0abdb79 Enable 'ko' locale for Korean in app.constants.ts 2020-12-08 15:59:00 +09:00
wiz
29b9fec3b9 Enable 'ko' locale for Korean in angular.json 2020-12-08 15:57:46 +09:00
softsimon
40ac83ee88 Update strings from transifex. 2020-12-08 11:58:43 +07:00
softsimon
5659a8c086 Correcting merge conflict. 2020-12-08 11:57:03 +07:00
softsimon
f9e8dfb079 Merge branch 'master' into simon/angular-universal
* master: (42 commits)
  i18n: Added missing Bisq translations. Minor missing space and character fixes.
  add missing 'sat/vb' string for i18n
  i18n: Removed CR from "In X minutes" translation
  i18n: Updated strings for "x confirmations", "x transactions", "x blocks" and "block ETA". Peg-out, and bisq headers.
  Update translation strings from Transifex
  Enable 'ka' locale for Georgian
  Update translation strings from Transifex
  Disable Vietnamese locale 'vi' until translations are completed
  Update translation strings from Transifex
  add 'sat' string for i18n
  Update translation strings from Transifex
  Update translation strings from Transifex
  i18n update
  Remove extra garbage characters from OP_RETURN tooltip. fixes #254
  Fix for changing locale on other networks than mainnet. fixes #253
  i18n: Asset search box
  Update translation strings from Transifex
  i18n: Added X of X transaction. Flipped collapse/expand
  i18n: Added "miner identification" and updated "navigate to sponsor"
  Update translations from Transifex
  ...

# Conflicts:
#	frontend/src/app/components/app/app.component.ts
#	frontend/src/app/components/mempool-block/mempool-block.component.ts
2020-12-08 00:11:38 +07:00
softsimon
ae886e14b8 i18n: Added missing Bisq translations. Minor missing space and character fixes. 2020-12-07 17:35:05 +07:00
wiz
8d5ccf94f3 Merge pull request #264 from TechMiX/addMissingTranslatableString
add missing 'sat/vB' string for i18n
2020-12-07 10:21:48 +09:00
TechMiX
91cb2adb95 add missing 'sat/vb' string for i18n 2020-12-07 00:07:16 +01:00
softsimon
f70f94fede i18n: Removed CR from "In X minutes" translation 2020-12-07 00:23:13 +07:00
wiz
1006adabce Merge pull request #263 from mempool/simon/i18n-shared
i18n: Updated strings for "x confirmations", "x transactions", "x blo…
2020-12-07 02:04:14 +09:00
softsimon
e2d19a174e i18n: Updated strings for "x confirmations", "x transactions", "x blocks" and "block ETA". Peg-out, and bisq headers. 2020-12-06 23:44:08 +07:00
wiz
c19e21449d Merge pull request #262 from TechMiX/addMissingTranslatableString
add 'sat' string for i18n
2020-12-07 01:07:05 +09:00
wiz
c819ddc64a Update translation strings from Transifex 2020-12-07 00:20:22 +09:00
wiz
a2740aaa02 Enable 'ka' locale for Georgian 2020-12-07 00:20:05 +09:00
wiz
8d565f5c62 Update translation strings from Transifex 2020-12-06 03:44:29 +09:00
wiz
f93e1c460a Disable Vietnamese locale 'vi' until translations are completed 2020-12-06 03:40:04 +09:00
wiz
0247c7137e Update translation strings from Transifex 2020-12-06 01:12:33 +09:00
TechMiX
159d9c71a1 add 'sat' string for i18n 2020-12-05 14:59:08 +01:00
wiz
805636229a Update translation strings from Transifex 2020-12-05 22:42:48 +09:00
wiz
c5cd80ac48 Update translation strings from Transifex 2020-12-05 21:57:51 +09:00
softsimon
94a5e8edae i18n update 2020-12-05 11:58:22 +07:00
softsimon
1af1f7211e Remove extra garbage characters from OP_RETURN tooltip.
fixes #254
2020-12-05 02:32:16 +07:00
softsimon
4d54723472 Fix for changing locale on other networks than mainnet.
fixes #253
2020-12-05 02:08:54 +07:00
softsimon
1fc1b4b86e i18n: Asset search box 2020-12-05 02:01:31 +07:00
wiz
5774b395a7 Update translation strings from Transifex 2020-12-05 02:39:50 +09:00
softsimon
f4c27e4c26 i18n: Added X of X transaction. Flipped collapse/expand 2020-12-05 00:16:23 +07:00
softsimon
336e45a7b1 i18n: Added "miner identification" and updated "navigate to sponsor" 2020-12-04 23:07:53 +07:00
wiz
6acacc7792 Update translations from Transifex 2020-12-05 00:36:00 +09:00
softsimon
10b9778b3c Adding missing Asset page strings. Fixed block title translations. 2020-12-04 22:30:09 +07:00
wiz
9ebb9a5d65 Use --force when running tx pull for Transifex strings 2020-12-05 00:12:08 +09:00
wiz
6476d1e77c Update translations from Transifex 2020-12-05 00:11:40 +09:00
softsimon
67df21fe50 Fixing about-link title translation. 2020-12-04 21:49:03 +07:00
wiz
1e8167212f Merge pull request #251 from mempool/simon/i18n-everything
i18n all the remaining strings.
2020-12-04 23:40:35 +09:00
softsimon
ea496915c4 Updated messages.xlf 2020-12-04 21:38:14 +07:00
softsimon
035f9a4d67 Merge commit '768fbdfbfa8e0064b86e168c59762909f4c1763b' into simon/i18n-everything
* commit '768fbdfbfa8e0064b86e168c59762909f4c1763b':
  Update translated strings from Transifex
  More fixes for RTL
  Always render app-mempool-graph using LTR
  Implement auto RTL support for i18n locales like Arabic, Persian, etc.
  Update extracted i18n strings from source
  Enable 'ar' locale for Arabic
  Update translated strings from Transifex
  Fix incorrectly used 'nn' locale, rename it to 'nb' instead
  Update transifex-client configuration to use master branch

# Conflicts:
#	frontend/src/locale/messages.xlf
2020-12-04 21:30:51 +07:00
softsimon
232fc65af2 i18n correcctions based on feedback. Added even more missing i18n strings. 2020-12-04 21:29:31 +07:00
wiz
768fbdfbfa Update translated strings from Transifex 2020-12-04 23:03:52 +09:00
softsimon
974d89e40b Merge pull request #257 from mempool/wiz/implement-rtl-support-for-i18n
Implement auto RTL support for i18n locales like Arabic, Persian, etc.
2020-12-04 21:00:39 +07:00
wiz
92d163f495 More fixes for RTL 2020-12-04 22:54:09 +09:00
wiz
62363802be Always render app-mempool-graph using LTR 2020-12-04 22:41:58 +09:00
wiz
e49ecd9368 Implement auto RTL support for i18n locales like Arabic, Persian, etc.
Fixes #255
2020-12-04 05:56:40 +09:00
wiz
ea521651fd Update extracted i18n strings from source 2020-12-04 04:11:35 +09:00
wiz
73db9c5023 Enable 'ar' locale for Arabic 2020-12-04 04:11:14 +09:00
wiz
cc451d29ab Update translated strings from Transifex 2020-12-04 04:07:01 +09:00
wiz
a831d04694 Fix incorrectly used 'nn' locale, rename it to 'nb' instead 2020-12-03 22:37:06 +09:00
softsimon
fb11d73751 i18n all the remaining strings. 2020-12-03 18:34:19 +07:00
wiz
9509891abc Update transifex-client configuration to use master branch 2020-12-03 20:09:08 +09:00
wiz
129f122993 Replace nginx.conf try_files for /$lang/index.html with rewrite (fixes #250) 2020-12-03 18:52:03 +09:00
wiz
8875579c08 Update strings from Transifex 2020-12-03 10:16:21 +09:00
wiz
83501cfdbe Enable locale 'vi' for Vietnamese 2020-12-03 10:16:02 +09:00
wiz
2b2bc2fc41 Change nginx.conf Tor HS port from 81 to 80 to fix i18n bug 2020-12-03 09:56:29 +09:00
wiz
6b498b9601 Exempt localhost from nginx rate limits for Tor and AU usage 2020-12-03 09:47:39 +09:00
softsimon
e35eb5aad2 Adding virtual size to transaction details.
fixes #248
2020-12-02 22:22:14 +07:00
softsimon
2bba51e6c1 Merge branch 'master' into simon/angular-universal
* master:
  Update list of supported locales
  Add one more fix to nginx.conf for i18n
  Remove unused i18n tags in frontend/src/index.html
  Update translations from Transifex
  Enable 'fr' locale for French
  Corrected some missing spaces on transactions page and a blank render bug when confirmation time is below 1 minute.
  Implement i18n support in frontend using Angular + Transifex + NGINX

# Conflicts:
#	frontend/src/app/app.constants.ts
#	frontend/sync-assets.js
2020-12-02 20:05:49 +07:00
wiz
d3cb1d607b Update list of supported locales 2020-12-02 21:33:53 +09:00
wiz
352afbc029 Add one more fix to nginx.conf for i18n 2020-12-02 21:21:44 +09:00
softsimon
2ef87603fd Merge pull request #191 from mempool/wiz/i18n
Implement i18n support in frontend using Angular + Transifex + NGINX
2020-12-02 19:07:28 +07:00
wiz
88b702d6d8 Remove unused i18n tags in frontend/src/index.html 2020-12-02 20:35:52 +09:00
wiz
0d2e4000e1 Update translations from Transifex 2020-12-02 20:35:50 +09:00
wiz
9148d30fcc Enable 'fr' locale for French 2020-12-02 20:35:46 +09:00
softsimon
3f990ff706 Corrected some missing spaces on transactions page and a blank render bug when confirmation time is below 1 minute. 2020-12-02 17:03:04 +07:00
wiz
4658b47007 Implement i18n support in frontend using Angular + Transifex + NGINX
This PR adds basic i18n support into the mempool frontend, together with
a smooth workflow for developers and translators to collaborate:

* Using the existing @angular/localize module, developers add i18n
metadata to any frontend strings their new features or changes modify

* Using the new npm script `i18n-extract-from-source`, developers
extract the i18n data from source code into `src/locale/messages.xlf`

* After pushing the updated `src/locale/messages.xlf` to GitHub, the
Transifex service will update its database from the new source data

* Using the Transifex website UI, translators can work together to
translate all the mempool frontend strings into their native languages

* Using the new npm script `i18n-pull-from-transifex`, developers can
pull in completed translations from Transifex, and commit them into git.

This flow requires an API key from Transifex, which can be obtained at
https://www.transifex.com/user/settings/api/ to be used with the python
script installed by `pip install transifex-client` - after preparing
these, run the npm script which will ask you for the API key the first
time. When downloading is complete, you can test building the frontend,
and if successful, commit the new strings files into git.

This PR implements a new locale selector in the footer of the homepage
dashboard, and includes WIP translations for the following languages:

* Czech (cs)
* German (de)
* Japanese (ja)
* Norwegian (nn)
* Spanish (es)
* Swedish (sv)
* Ukrainian (uk)
* Persian (fa)
* Portugese (pt)
* Turkish (tr)
* Dutch (nl)
* French (fr)
* Chinese (zh)
* Slovenian (sl)
* Korean (ko)
* Polish (pl)

The user-agent's `Accept-Language` header is used to automatically
detect their preferred language, which can be manually overriden by the
pull-down selector, which saves their preference to a cookie, which is
used by nginx to serve the correct HTML bundle to the user.

Remaining tasks include adding i18n metadata for strings in the Bisq and
Liquid frontend code, mouseover hover tooltip strings, hard-coded og
metadata inside HTML templates, and many other places. This will be done
in a separate PR.

When upgrading to add i18n support, mempool instance operators must take
care to install the new nginx.conf and nginx-mempool.conf files, and
tweak for their specific site configuration.

Fixes #81
2020-12-02 04:19:33 +09:00
softsimon
f9c2bc1fb5 Merge commit 'f151eb81c812a62d1d5c57ee91620a3dd9ae5068' into simon/angular-universal
* commit 'f151eb81c812a62d1d5c57ee91620a3dd9ae5068':
  Upgrade angular from 10.0.4 to 10.2.3, typescript from 3.9.7 to 4.0.5
  Modify /bisq/api/v1/markets/markets to only return active markets
  Remove unnecessary @types/axios dependency.
  Same npm fix for running `tsc` from `./node_modules` for backend
  Set npm to run `ng` and `tsc` binaries from `./node_modules/` path

# Conflicts:
#	frontend/package-lock.json
#	frontend/package.json
2020-11-28 02:21:12 +07:00
softsimon
f151eb81c8 Merge pull request #195 from mempool/wiz/angular-10.2.3
Upgrade angular from 10.0.4 to 10.2.3, typescript from 3.9.7 to 4.0.5
2020-11-28 02:09:52 +07:00
wiz
9b89d9977f Upgrade angular from 10.0.4 to 10.2.3, typescript from 3.9.7 to 4.0.5 2020-11-28 04:04:45 +09:00
wiz
b6150a3237 Route all Angular Universal requests to nginx, remove simon's hacks 2020-11-27 23:03:48 +09:00
softsimon
dee9fa2d9a Hack to fix the urls. 2020-11-27 20:11:15 +07:00
softsimon
42d1fcbdee Fix for non matching request URLs. 2020-11-27 20:01:25 +07:00
softsimon
d0da875e69 Adding ServerTransferStateModule for AU with custom http interceptor for cache transfer. 2020-11-27 18:41:57 +07:00
softsimon
c7abb7a08a Merge pull request #189 from mempool/only-return-active-bisq-markets
Modify /bisq/api/v1/markets/markets to only return active markets
2020-11-23 22:02:13 +07:00
softsimon
5248fadcea removing /api again 2020-11-23 21:59:30 +07:00
wiz
d613aec395 Modify /bisq/api/v1/markets/markets to only return active markets
As per https://github.com/bisq-network/bisq/issues/4826 and https://github.com/bisq-network/bisq/pull/4831
2020-11-23 22:46:14 +09:00
softsimon
9e716aa913 Remove unnecessary @types/axios dependency. 2020-11-23 18:36:55 +07:00
softsimon
2cbf1474c3 New frontend config for webserver url to fix static asset httpGet.
Removed "absolute" from config names.
2020-11-23 18:22:21 +07:00
wiz
824bc21035 Same npm fix for running tsc from ./node_modules for backend 2020-11-23 17:26:22 +09:00
wiz
e51a4ce028 Set npm to run ng and tsc binaries from ./node_modules/ path 2020-11-23 17:19:16 +09:00
softsimon
2054c951ae Remove ssr prepended url from asset service. 2020-11-23 15:04:41 +07:00
wiz
5a70234370 Don't append /api to electrs backend path for SSR 2020-11-23 16:53:44 +09:00
softsimon
e5959f14bc Update absolute url config variables. 2020-11-23 14:50:28 +07:00
softsimon
aaf9d9be9f Merge branch 'master' into simon/angular-universal
* master:
  correcting merge
  Remove extra space in price server URL
  Modify upgrade script to append repo before tag name
  Merge "getInitData" method from simon/angular-universal.
  Replace opennode usd price source with wiz api. fixes #166
  Display confidential instead of nothing for confidential assets. fixes #110
  Detect confidential assets and display properly. fixes #109
  Allow searching for and viewing assets not in the asset registry. fixes #111

# Conflicts:
#	backend/src/api/websocket-handler.ts
2020-11-23 14:08:50 +07:00
wiz
fdf9cf7977 Merge pull request #186 from mempool/simon/update-pricefeed
Replace opennode usd price source with wiz api.
2020-11-23 16:06:15 +09:00
softsimon
bd67eec777 correcting merge 2020-11-23 11:46:04 +07:00
wiz
2fd559a7e1 Merge pull request #185 from mempool/simon/liquid-fixes
Liquid asset display bug fixes
2020-11-23 10:11:43 +09:00
wiz
9059c0c7e4 Merge remote-tracking branch 'origin/master' into simon/update-pricefeed 2020-11-23 09:56:23 +09:00
wiz
8fab153fb0 Remove extra space in price server URL 2020-11-23 09:48:06 +09:00
wiz
827c5d12a3 Modify upgrade script to append repo before tag name 2020-11-23 09:46:50 +09:00
softsimon
c0d2430a84 Merge "getInitData" method from simon/angular-universal. 2020-11-23 02:38:56 +07:00
softsimon
c36addd8c1 Frontend config support for AU. New absolute server url settings.
refs #104
2020-11-23 02:30:46 +07:00
softsimon
83126b83f1 Transaction, address and block page now works. 2020-11-22 23:47:27 +07:00
softsimon
75726df275 Merge branch 'master' into simon/angular-universal
* master:
  Tweak ASM opcode styling colors
  Add some color and styling to the Bitcoin ASM opcodes
  Correcting title text on graph invert button.
  Modify upgrade script to include "tag @ hash" in notification msg
  Adding a button to invert the graph globally.
  Display P2PK instead of OP_RETURN fixes #161
  Improved utxo script design. fixes #46
  Adding prevout script. Fixed padding. refs #46
  Correcting details button padding on mobile.
  Fix nginx.conf reverse proxy cache URL path for sponsor images
  Add missing "engines" metadata into package-lock.json
  Upgrade backend/package-lock.json to version 2
  Toggle display UTXO details and scripts for transactions fixes #46
  Axios error handle sponsor proxy requests.
  Replacing request.js with axios fixes #153
  Add basic websocket error handler as emergency fix for site crashing

# Conflicts:
#	frontend/src/app/services/storage.service.ts
2020-11-22 20:53:12 +07:00
softsimon
1521d47cc7 Replace opennode usd price source with wiz api.
fixes #166
2020-11-22 16:48:55 +07:00
softsimon
6bc6966019 Display confidential instead of nothing for confidential assets.
fixes #110
2020-11-22 16:30:36 +07:00
softsimon
01689c8433 Detect confidential assets and display properly.
fixes #109
2020-11-22 16:19:57 +07:00
softsimon
11d67cf756 Allow searching for and viewing assets not in the asset registry.
fixes #111
2020-11-22 16:03:23 +07:00
wiz
30fb0bad78 Tweak ASM opcode styling colors 2020-11-21 18:17:29 +09:00
softsimon
742ce7c429 Merge pull request #184 from mempool/wiz/style-asm-opcodes
Add some color and styling to the Bitcoin ASM opcodes
2020-11-21 03:33:54 +07:00
wiz
9e83fdc9f2 Add some color and styling to the Bitcoin ASM opcodes
Fixes #164
2020-11-21 05:29:50 +09:00
softsimon
36022680cb Merge pull request #183 from mempool/wiz/angular-universal
Update `og:title` meta tag when changing page title
2020-11-20 00:46:04 +07:00
wiz
a19ff7d3a7 Update og:title meta tag when changing page title 2020-11-20 00:59:10 +09:00
softsimon
9f9c0b1114 Correcting title text on graph invert button. 2020-11-16 21:58:16 +07:00
wiz
ab6ed227e3 Modify upgrade script to include "tag @ hash" in notification msg 2020-11-16 23:44:32 +09:00
wiz
e029f91d4c Merge pull request #162 from mempool/simon/graph-inverter
Adding a button to invert the graph globally.
2020-11-16 21:37:57 +09:00
softsimon
ca540d902a Adding a button to invert the graph globally. 2020-11-16 19:27:06 +07:00
softsimon
76238f5943 Display P2PK instead of OP_RETURN
fixes #161
2020-11-16 16:21:37 +07:00
softsimon
d9803e3f3d Improved utxo script design.
fixes #46
2020-11-16 02:25:10 +07:00
softsimon
c9e63a723a Adding prevout script. Fixed padding.
refs #46
2020-11-16 02:02:16 +07:00
softsimon
4136e8d332 Correcting details button padding on mobile. 2020-11-16 01:10:07 +07:00
wiz
ffb08e5e01 Fix nginx.conf reverse proxy cache URL path for sponsor images 2020-11-16 00:43:10 +09:00
wiz
c24e44f6fa Merge pull request #158 from mempool/simon/axios
Replacing request.js with axios
2020-11-16 00:37:43 +09:00
wiz
1676a78c13 Merge remote-tracking branch 'origin/master' into simon/axios 2020-11-15 22:17:11 +09:00
wiz
379db7b211 Merge pull request #159 from mempool/simon/utxo-details
Toggle display UTXO details and scripts for transactions
2020-11-15 22:11:44 +09:00
softsimon
e01718db22 Merge pull request #160 from mempool/wiz/upgrade-package-lock-json
Upgrade backend/package-lock.json to version 2
2020-11-15 19:26:18 +07:00
wiz
df81035ebc Add missing "engines" metadata into package-lock.json 2020-11-15 21:17:24 +09:00
wiz
0b35f784c4 Upgrade backend/package-lock.json to version 2 2020-11-15 20:47:36 +09:00
softsimon
ddea10b160 Toggle display UTXO details and scripts for transactions
fixes #46
2020-11-15 17:58:38 +07:00
softsimon
0616b3c3b0 Axios error handle sponsor proxy requests. 2020-11-15 14:31:34 +07:00
softsimon
e7ddedaeb6 Replacing request.js with axios
fixes #153
2020-11-15 14:22:47 +07:00
wiz
12b3ecd078 Add basic websocket error handler as emergency fix for site crashing 2020-11-15 11:44:16 +09:00
softsimon
f8ffd6ec6b Merge branch 'master' into simon/angular-universal
* master:
  Add the Commons Clause License Condition v1.0 to our MIT License
  fix legend bug on resize
  Correcting new chartist import path.
  Flip mempool chart by using patched chartist package by @techmix

# Conflicts:
#	frontend/package-lock.json
2020-11-15 02:51:00 +07:00
softsimon
1e39daafb3 Merge pull request #154 from TechMiX/fixGraphLegendBug
Fix graph legend bug on resize
2020-11-15 00:39:22 +07:00
wiz
c03f5d8393 Merge pull request #155 from mempool/add-commons-clause-license-condition
Add the Commons Clause License Condition v1.0 to our MIT License
2020-11-15 00:15:16 +09:00
wiz
65c29ddff2 Add the Commons Clause License Condition v1.0 to our MIT License 2020-11-14 22:39:03 +09:00
TechMiX
640a77e846 fix legend bug on resize 2020-11-13 01:23:19 +01:00
softsimon
5aa17e001c Correcting new chartist import path. 2020-11-10 21:09:50 +07:00
softsimon
ff8f2fafe8 Flip mempool chart by using patched chartist package by @techmix 2020-11-10 21:05:54 +07:00
softsimon
ea04dfa62f Merge pull request #151 from mempool/wiz/angular-universal
Various fixes (lol)
2020-11-08 00:39:44 +07:00
wiz
0361044352 Various fixes (lol) 2020-11-08 01:16:09 +09:00
softsimon
b39d12f64e Merge branch 'master' into simon/angular-universal
* master:
  Enable nginx reverse proxy cache for sponsor profile images + misc tweaks
  Update Bisq price API: markets.bisq.network -> bisq.markets
2020-11-07 14:03:03 +07:00
softsimon
14c9d0c409 angular universal WIP
refs #104
2020-11-07 04:30:52 +07:00
wiz
b89a549a75 Enable nginx reverse proxy cache for sponsor profile images + misc tweaks 2020-11-06 14:09:04 +09:00
wiz
203374bce2 Update Bisq price API: markets.bisq.network -> bisq.markets 2020-11-05 13:59:58 +09:00
softsimon
d8c4f5a6ac Adding some padding and correcting a bad pixel line. 2020-11-03 18:03:25 +07:00
softsimon
1877b40413 Use actual mempool size instead of mempool vsize.
fixes #150
2020-11-03 16:11:32 +07:00
wiz
6f6d7bc4d2 ❤️ 2020-11-03 17:03:41 +09:00
wiz
f1463b914d Modify index.html metadata to have timestamp for mempool-preview.png URL 2020-11-03 16:44:25 +09:00
softsimon
c03073bddd FIx: Write cache sync on exit. 2020-11-02 22:06:51 +07:00
softsimon
ce582eefc6 Fix for kept block amount when cache already contains more. 2020-11-02 21:28:40 +07:00
softsimon
d46ff35dfb Fix: Split mempool disk cache into 50K txs per file 2020-11-02 19:11:04 +07:00
softsimon
6865e00738 Fix: Reduce red difficulty adjustment progress when nearing end of cycle. 2020-11-02 14:48:48 +07:00
softsimon
73acec23ae Mute websocket parse error. 2020-11-02 01:14:42 +07:00
softsimon
72325b683e Merge pull request #147 from TechMiX/master
Save the legends status in cache
fixes #70
2020-11-01 20:26:38 +07:00
softsimon
8dd257aebd Upgrading social media preview image to new design. 2020-11-01 17:40:45 +07:00
softsimon
b21016efef Removing unnused config MINED_BLOCKS_CACHE. 2020-10-31 18:58:34 +07:00
softsimon
200e68f15a Fix: Wrong variable used to display mempool size. 2020-10-29 07:06:54 +07:00
TechMiX
411b75471c Merge branch 'master' of https://github.com/mempool/mempool 2020-10-28 22:25:33 +01:00
TechMiX
5d7a39a8f2 save the legends active status on a cache 2020-10-28 22:22:07 +01:00
softsimon
fcb51fef20 Calculate tx fees based on segwit tx sizes. 2020-10-28 20:26:48 +07:00
softsimon
5feaff130f Adding missing error cause messages. 2020-10-28 11:00:48 +07:00
softsimon
29ff029b07 Updating lightning node name.
Fixing dashboard button alignment.
2020-10-27 19:27:48 +07:00
softsimon
6494d6daf8 Removing hxpress http compression 2020-10-27 17:42:10 +07:00
softsimon
396ff6a375 Change collapsing dashboard into 3 levels. 2020-10-27 16:34:27 +07:00
softsimon
e61574c630 Add L-BTC sponsor support. 2020-10-27 15:26:37 +07:00
softsimon
edf2d4205d Feature: Add a collapse/show advanced view feature on the Dashboard
fixes #134
2020-10-27 02:58:29 +07:00
softsimon
645772c01a Fix: Segwit-tag is inconsistent when transaction contains segwit and non segwit
fixes #130
2020-10-27 02:07:39 +07:00
softsimon
d260a1ed73 Write cache.json on startup if it doesn't exist.
Write mempool and block data to cache on every new block
fixes #146
2020-10-27 00:05:06 +07:00
softsimon
6a8deff706 Adding form validators for donation form. 2020-10-26 23:23:08 +07:00
softsimon
018e95e648 Fixing missing dashboard graph labels in Firefox. 2020-10-26 23:12:32 +07:00
softsimon
1fc4e9530d Handle weird response body when fetching Bisq price.
fixes #145
2020-10-26 19:33:20 +07:00
wiz
9a94fccf40 Revert "Lock NPM versions."
This reverts commit b544af14e4.
2020-10-26 20:56:55 +09:00
softsimon
99162f5ec9 Sponsor Lightning support. 2020-10-26 18:29:25 +07:00
softsimon
b544af14e4 Lock NPM versions. 2020-10-26 15:59:28 +07:00
wiz
75ce300332 Don't call npm install with --only=prod 2020-10-26 04:24:05 +09:00
softsimon
fb47f5606a npm audit fix 2020-10-26 01:13:36 +07:00
softsimon
9d7b52a104 Removing AVG() from mysql query reducing execution time. Increasing graph data span.
fixes #106
2020-10-26 01:00:21 +07:00
wiz
c5c2d67fce Don't rebuild backend on every restart 2020-10-25 22:22:34 +09:00
softsimon
53bc7725ab Merge pull request #143 from TechMiX/master
update legend state after chart update
2020-10-24 00:27:09 +07:00
TechMiX
0fe32835c9 update legend state after chart update 2020-10-23 19:03:15 +02:00
wiz
aa8d3798ea API Docs: move Fee Estimate APIs to their own tab 2020-10-23 22:15:17 +09:00
softsimon
b4a17693af Only log warning if main update loop has failed more than once. 2020-10-23 16:27:02 +07:00
wiz
c6d0571be8 Merge pull request #138 from lucasmoten/patch-1
Update README with repository checkout guidance
2020-10-20 11:54:32 +09:00
wiz
56b551ea8e Update README to get latest release tag using portable parser 2020-10-20 11:54:04 +09:00
softsimon
bdeac55c97 Fix for statistics enabled config. 2020-10-19 20:24:22 +07:00
wiz
53f7839131 Don't append btc_mainnet/db/ to BISQ_MARKETS.DATA_PATH 2020-10-19 20:58:25 +09:00
wiz
32095cd6b3 Don't rate limit /api/v1/donations URL prefix for sponsor photos 2020-10-19 20:56:46 +09:00
wiz
d8521a9e21 Update production backend configuration files for all 4 instances 2020-10-19 20:40:56 +09:00
softsimon
962c024ecb Use default configs.
refs #141
2020-10-19 18:36:29 +07:00
wiz
4c82e292be Fix typo in previous commit 2020-10-19 19:49:02 +09:00
wiz
eb00b92996 Modify upgrade script to allow reverting to a specific git hash or tag 2020-10-19 19:43:30 +09:00
softsimon
cbef2ae6d0 Limit mempool clear protection to >20K TX mempool
fixes #140
2020-10-19 17:30:47 +07:00
softsimon
45efb604c1 Revamping configuration file.
fixes #141
2020-10-19 11:57:02 +07:00
softsimon
0abe62128e Reducing retry time to max 60 seconds 2020-10-18 22:48:15 +07:00
softsimon
49f70ca28a Slow down retries on Electrs errors in the main mempool loop. 2020-10-18 21:47:47 +07:00
softsimon
2ba7cd9ebd Proxy image requests. 2020-10-18 21:37:27 +07:00
wiz
70da8248cc Remove last paragraph on about page 2020-10-18 20:02:48 +09:00
wiz
716b1235ee Fix typo on about page text 2020-10-18 19:54:48 +09:00
wiz
e732f0f1dc Tweak about page text for project description 2020-10-18 19:28:11 +09:00
softsimon
fff8120daa Backend API to load sponsor profile photos. 2020-10-18 17:14:35 +07:00
wiz
9d4659c3ba Add Terms of Service links with minor tweaks to text formatting 2020-10-18 16:03:03 +09:00
wiz
95bb0fc265 Fix Bisq API docs page from not expanding on load 2020-10-18 13:53:45 +09:00
wiz
2c3f425797 Route /api and /<network/api URLs to HTML for API documentation 2020-10-18 13:11:02 +09:00
wiz
fb9b3202ca Add basic API documentation for all electrs endpoints 2020-10-18 12:56:08 +09:00
softsimon
7bf9810c48 Run Bisq block parse in separate Worker Thread. 2020-10-17 18:13:09 +07:00
softsimon
2715d02cf9 Fix exception handling. Don't save < 0.01 donations to DB.
refs #133
2020-10-17 15:01:58 +07:00
wiz
0fca6f3a3b Tweak about page spacing and text 2020-10-17 16:13:57 +09:00
wiz
2d3ed5f8cb Tag sponsored links with rel=sponsored, don't tag our social media links 2020-10-17 13:31:41 +09:00
wiz
988ed7e8af Add some brief text to our about page, state Bitcoin Only policy 2020-10-17 12:25:44 +09:00
wiz
c22d6e741a Change DB structure for donations: int -> varchar, blob -> mediumblob 2020-10-17 11:32:42 +09:00
softsimon
15fdb69b96 Use Twitter API to fetch profile photos.
fixes #133
2020-10-16 16:29:54 +07:00
softsimon
e7a7b45ad0 Readding "BISQ_ENABLED". Display 'bisq' in logs. 2020-10-15 11:54:21 +07:00
softsimon
d8857f1073 Replacing BISQ_ENABLED with NETWORK === "bisq" 2020-10-15 11:37:25 +07:00
softsimon
b767a0a33e Allow bisq network to be "bisq". 2020-10-15 11:15:04 +07:00
softsimon
472b1d35c2 Improve some logger error messages. 2020-10-15 11:12:33 +07:00
softsimon
86c654f22f New backend config "NETWORK".
Only activate mempool protection.
Log network to
fixes #140
2020-10-15 11:07:53 +07:00
softsimon
372c116283 Input sanitation. Minimum amount fix. Debug log updated. 2020-10-13 19:54:47 +07:00
softsimon
94e06a3a6b Add notice for BTCpay invoices and requests.
refs #135
2020-10-13 18:26:10 +07:00
wiz
3fd5277912 Update mempool-logger and syslog.conf to separate info and notice msgs 2020-10-13 19:58:11 +09:00
wiz
056fe5712d Add newsyslog.conf to rotate mempool logfiles regularly 2020-10-13 19:56:10 +09:00
softsimon
4399c5e8e9 Changing more loggings levels.
refs #135
2020-10-13 17:48:43 +07:00
wiz
cbcfbe5b36 Add mempool-logger script and syslog.conf configuration to log to it 2020-10-13 19:16:09 +09:00
softsimon
b98b979dc8 Changing more loggings levels.
refs #135
2020-10-13 16:58:48 +07:00
softsimon
3d374fd9d9 Changing more loggings levels.
refs #135
2020-10-13 16:43:09 +07:00
softsimon
4a14085908 Use persistent dgram socket client.
refs #135
2020-10-13 16:03:03 +07:00
softsimon
04ac820ed7 Changing common output messages to Debug.
refs #135
2020-10-13 16:00:58 +07:00
softsimon
4dacf292c2 Adding logger wrapper.
Log backend messages to syslog.
fixes #135
2020-10-13 15:27:52 +07:00
softsimon
4c203631db Making order_id (BTCPay invoice id) unique in the database. 2020-10-13 10:10:46 +07:00
Lucas Moten
ed3811bf2c Update README with repository checkout guidance
- Changed repository to clone based on updated organization
- Determine latest release from github api call.  An alternative form being `latestrelease=$(git describe --exact-match --abbrev=0)` to yield the most recent tag name.
2020-10-12 22:51:39 +00:00
softsimon
55646b5732 Handle and don't crash for adress-prefix search error.
fixes #121
2020-10-12 12:57:19 +07:00
wiz
f5e270c770 Use https:// instead of http:// to query mempool.space API 2020-10-12 14:26:01 +09:00
softsimon
e37a9de71d Proxy donation requests to mempool.space when BTCPay Sponsors is disabled. 2020-10-12 10:50:10 +07:00
wiz
7673bf13b9 Update terms of service, add Sponsor Policy for inappropriate profiles 2020-10-12 08:37:56 +09:00
wiz
88cb0d020d Merge pull request #132 from mempool/wiz/terms-of-service
Add new Terms of Service page with links and tweak About page layout for it
2020-10-11 20:58:40 +09:00
wiz
db16dbbc9d Apply suggestions from code review
Co-authored-by: softsimon <softsimon@users.noreply.github.com>
2020-10-11 20:57:52 +09:00
wiz
1d8d39db6b Only show first 8 characters of git commit hash on about page 2020-10-11 20:20:48 +09:00
wiz
024b2d58f7 Move social media icons to bottom, add terms of service links 2020-10-11 20:15:56 +09:00
wiz
bb3842fc10 Add basic Terms of Service page 2020-10-11 19:23:52 +09:00
wiz
8cd98b42fe Fix matomo URL filename 2020-10-10 11:55:27 +09:00
wiz
c8d22dc536 Load self-hosted matomo analytics stats on mempool.space domain 2020-10-10 11:42:32 +09:00
softsimon
a8a1f4e976 About page updates.
refs  #128
2020-10-09 13:56:43 +07:00
wiz
ba315648be Update top-level README screenshot and v2 guide 2020-10-09 06:10:23 +09:00
wiz
761eff62c5 Merge pull request #129 from mempool/donations-fixes
Enhancements to the sponsorship GUI
2020-10-09 04:20:35 +09:00
wiz
0e5f2dd1a4 Use higher resolution profile photos and other minor tweaks 2020-10-09 04:19:32 +09:00
softsimon
dfda0d1890 Bugfix: JS error in transactions.component
fixes #126
2020-10-08 17:53:02 +07:00
softsimon
784f00b725 Enhancements to the sponsorship GUI
fixes  #128
2020-10-08 17:51:10 +07:00
wiz
ad144a34ac Handle btcpay "paid" invoice status for sponsorship donations 2020-10-08 04:46:11 +09:00
wiz
9255f1c007 Merge pull request #125 from mempool/donations
Custom BTCPay donation integration
2020-10-08 02:57:48 +09:00
softsimon
87dc1e5db4 Check and accept both 'complete' and 'confirmed' invoices.
refs #122
2020-10-08 00:51:11 +07:00
softsimon
1f7483687f Don't allow invoices lower than 0.001 and require 0.01 for sponsorship.
refs #122
2020-10-08 00:20:42 +07:00
softsimon
0cbc7e2ab6 Make BTCPay webhook configurable.
refs #122
2020-10-08 00:15:26 +07:00
wiz
c48a151e21 Tweak upgrade script to take PR branch name as argument 2020-10-08 01:35:54 +09:00
softsimon
5b8dbfca74 Addig fronend flag (SPONSORS_ENABLED) to enable Sponsors in the gui.
refs #122
2020-10-07 23:30:45 +07:00
softsimon
b6738dd9e8 Moving API docs to separate /api page. 2020-10-07 23:24:01 +07:00
softsimon
0ee2753100 Only display About page on main Bitcoin network. 2020-10-07 20:27:22 +07:00
softsimon
17dd03682b Set 0.01 BTC as minimum donation limit
refs #122
2020-10-07 20:19:22 +07:00
softsimon
a07a4de255 Custom BTCPay donation integration
fixes #122
2020-10-07 20:15:42 +07:00
softsimon
774893f2fc Bugfix: Incoming transactions not parsed properly. 2020-10-06 13:31:33 +07:00
softsimon
98c398272c Fix for incorrect division in fee estimation.
refs #65
2020-10-05 16:26:39 +07:00
softsimon
beee916658 Updating package lock to fix potential vulns. 2020-10-05 12:31:56 +07:00
softsimon
15bb5a966b Improved empty mempool as well as medium and low fee estimations.
refs #65
2020-10-05 11:42:54 +07:00
softsimon
766bd0d1e0 Adding missing 'irregular' Bisq transaction type.
fixes  #118
2020-10-04 17:51:21 +07:00
wiz
a438ba9fcb Add btcpay donation iframe widget on about page for testing 2020-09-30 22:27:13 +09:00
wiz
5332e4765f Add .github/FUNDING.yml for sponsor button link URL configuration 2020-09-30 22:14:27 +09:00
softsimon
c0ad643d42 DiskCache fix when using clustering. 2020-09-30 00:25:43 +07:00
softsimon
140fc0c5e1 Bugfix: Mempool and blocks disk cache not saved on exit when using cluster. 2020-09-29 23:13:22 +07:00
softsimon
7c6c330b02 Bugfix: Websocket data push stopped working on disconnect, bug caused by 3dedf1e3 2020-09-29 20:52:32 +07:00
softsimon
e8de73cfbc Correct timer to get actual time to update bisq market data. 2020-09-29 19:38:01 +07:00
softsimon
d0b3b240e6 Bugfix for js error on transaction page.
Increase ping-pong timeout.
2020-09-29 18:27:44 +07:00
softsimon
05bea21cc8 Fix: Weight per second should be vBytes per second. 2020-09-29 15:07:04 +07:00
softsimon
0f72030d5e Enhancement: "Load more" is now "Load all"
fixes #117
2020-09-29 15:05:52 +07:00
softsimon
dfaa73803e Bugfix: Liquid asset issuance / circulating / burned amounts are missing decimal point
fixes #112
2020-09-29 03:54:56 +07:00
softsimon
64244228ea Dashboard ux fixes. 2020-09-28 23:05:42 +07:00
softsimon
da5556e3dc Liquid assets display fix. 2020-09-28 16:44:35 +07:00
softsimon
b95efca29d Various bugfixes. 2020-09-28 16:32:48 +07:00
wiz
e9d3b44e97 Set "LIQUID":true in production liquid backend config 2020-09-28 05:00:21 +09:00
softsimon
7848481d8f Dashboard ux fixes. 2020-09-28 02:51:01 +07:00
softsimon
3e3dd83243 Fix: Show fee box on Testnet and Liquid.
Use 0.1 sat/vB as minimum liquid fee.
2020-09-27 18:12:36 +07:00
softsimon
3450de774f Fix for Bisq blocks data watchers stopps working randomly. Restaring watcher when block height has diverged. 2020-09-27 17:21:18 +07:00
softsimon
f4a78a0e78 Fix for double subscribing to stats data. 2020-09-27 13:39:38 +07:00
softsimon
5536e5e77d Minor ux updates to dashboard. 2020-09-27 02:24:50 +07:00
softsimon
dbc2f9e2dd Rearranged dashboard graphs. 2020-09-27 01:31:20 +07:00
softsimon
677cea329c Fixing titles and margins for dashboard. 2020-09-27 00:39:29 +07:00
softsimon
209865d23f Minor dashboard mobile view fixes.
Offline indicator position fix.
2020-09-27 00:18:44 +07:00
softsimon
fe2c9bf49d Revering comment. 2020-09-26 23:48:18 +07:00
softsimon
21ef5054bf Moving fee box into a regular box. 2020-09-26 23:40:26 +07:00
softsimon
3dedf1e3e1 Adding mini-graphs on dashboard. 2020-09-26 22:46:26 +07:00
softsimon
43314c2283 Correct dashboard for mobile. 2020-09-26 11:56:37 +07:00
softsimon
dd31cbfd70 Dashboard design updates based on pedro feedback. 2020-09-26 03:50:32 +07:00
softsimon
f9bbc425d8 Switching order of latest transactions and blocks. 2020-09-26 02:42:39 +07:00
softsimon
d4f768e3b6 Adding latest blocks and transactions to dashboard. 2020-09-26 02:11:30 +07:00
wiz
aa3559e634 Add production script to update local copy of assets registry JSON 2020-09-24 18:42:13 +09:00
wiz
8dddd6e25e Use mempool's fork of Blockstream's Liquid asset registry db repo 2020-09-24 17:12:53 +09:00
wiz
3aff874479 Update production nginx.conf and mempool-config.json for Bisq Markets API 2020-09-23 19:39:46 +09:00
wiz
fa75c1b08d Move backend restart command from upgrade command into its own command 2020-09-23 19:05:28 +09:00
softsimon
ea0edc41e2 Minor ux updates. 2020-09-23 16:48:10 +07:00
softsimon
89533cf76f Improved graph visibility on mobile. 2020-09-23 15:49:07 +07:00
softsimon
007bb30826 Bisq transactions dropdown menu position fix. 2020-09-23 01:43:56 +07:00
softsimon
9e71f1a683 Improved dashboard loading indicators. 2020-09-22 12:50:12 +07:00
softsimon
0464ad4bcf Hide Difficulty Epoch on Liquid. Updated difficulty change calc. 2020-09-22 12:26:54 +07:00
softsimon
bcf68aa074 Menu design updates. 2020-09-22 12:07:08 +07:00
softsimon
b2e0edb919 Menu design updates. 2020-09-22 06:55:45 +07:00
softsimon
ffee91939e Justify menu items center on mobile. 2020-09-22 06:19:14 +07:00
softsimon
9e3fad610c Removing hamburger menu on mobile. Adjusted icon sizes. 2020-09-22 06:13:27 +07:00
softsimon
b67b025dc2 Replacing menu button texts with icons.
Latest blocks is now a separate page.
2020-09-22 05:51:34 +07:00
softsimon
2c3b02a682 NodeJS cluster: Delay child process auto restart 10 seconds on exit.
refs #108
2020-09-22 04:27:08 +07:00
softsimon
6d67fbde84 NodeJS cluster fix: Only save disk cache to disk when master.
fixes #108
2020-09-22 04:07:47 +07:00
softsimon
347ab1e220 NodeJS cluster support. 2020-09-22 03:52:54 +07:00
softsimon
21e985202d Add difficulty percent change indicator. 2020-09-22 03:31:19 +07:00
softsimon
6c1d28a9ac Replacing footer and latest blocks with a stats dashboard. 2020-09-21 19:41:12 +07:00
wiz
8146939f0f Remove unnecessary options from production README's bitcoin.conf 2020-09-20 05:45:29 +09:00
softsimon
508b5c0f4e Markets API: Adding cache for general /ticker requests 2020-09-19 23:02:02 +07:00
softsimon
84f0ebaba6 Market api: /volume api fix. 2020-09-19 15:59:09 +07:00
wiz
04cc1338c0 Set npm dep locutus to dev 2020-09-19 02:25:51 +09:00
wiz
630e3fa863 Update production scripts to only install production npm deps 2020-09-19 02:19:47 +09:00
softsimon
5d2c0d2e0a Markets API: Use file mtime to only parse files that has updated. 2020-09-18 23:43:45 +07:00
wiz
55852bcf62 Update production bisq instance backend config for Bisq Markets data 2020-09-18 22:02:23 +09:00
wiz
91815072d5 Update production scripts to install/load NodeJS/npm using nvm 2020-09-18 21:53:58 +09:00
wiz
07cfdd73aa Fix upgrade script not restarting node due to new command line args 2020-09-18 18:08:59 +09:00
softsimon
059e4d079a Markets api: Add missing "timestamp" parameter for /hloc and /volumes 2020-09-17 15:38:25 +07:00
softsimon
14697a01cc Corrected Offers response data. 2020-09-17 02:45:14 +07:00
softsimon
4d4eaecb87 Market api: Fix memory leak 2020-09-15 23:12:42 +07:00
wiz
344413568d Increase NodeJS max heap to 4GB RAM for backend instance 2020-09-16 00:46:34 +09:00
softsimon
77cbb302ce Bisq market: Fix for missing prices from /trades 2020-09-15 12:37:54 +07:00
wiz
c1c0521ab4 Merge pull request #107 from mempool/bisq-markets
Bisq markets API
2020-09-15 05:28:28 +09:00
softsimon
a11135f358 * /volumes API
* /ticker API
2020-09-15 03:11:52 +07:00
softsimon
bafe2db094 HLOC markets api. 2020-09-13 17:51:53 +07:00
softsimon
98cc81c53d Corrections to parameters handling and sorting. 2020-09-12 01:26:51 +07:00
softsimon
60b1dd15f4 Fix pluralization for "transactions".
Display "genesis block" on block 0
2020-09-10 14:55:49 +07:00
softsimon
88e5b03430 Bisq Markets API. WIP 2020-09-10 14:46:23 +07:00
wiz
9234d23da2 Update production nginx.conf with performance tuning and rate limiting 2020-09-10 14:39:04 +09:00
softsimon
04351e843d Correcting robots.txt 2020-08-26 17:31:59 +07:00
softsimon
0041500e08 Correcting robots.txt 2020-08-26 17:24:40 +07:00
softsimon
4e8908925c Adding robots.txt 2020-08-26 17:05:29 +07:00
softsimon
80fc3df76d bugfix: vbytespersecond display was limited to 1667 2020-08-19 11:33:18 +07:00
softsimon
147de195a9 deleting yarn lock since we rely on NPM package-lock.json 2020-08-19 11:31:50 +07:00
softsimon
5300fb1265 Revert "Display whole fee span on mined blocks."
This reverts commit cb51b71128.
2020-08-13 02:53:52 +07:00
softsimon
71c44d725a Pagination fixes. 2020-08-13 02:35:10 +07:00
softsimon
097e2ba0ea Assets page pagination. 2020-08-13 00:46:44 +07:00
softsimon
b1c8166936 Update title to - "<network> Explorer" 2020-08-12 14:04:04 +07:00
softsimon
2d02ec7092 Return HTTP 503 from Fee Api when mempool is still syncing.
Fix for displaying git commit on About page.
2020-08-12 13:33:58 +07:00
softsimon
dd0b67716f Optimization for bisq transaction table rendering 2020-08-11 00:43:24 +07:00
softsimon
95bab64424 Fixes for bisq pagination and tx type select history 2020-08-11 00:01:22 +07:00
softsimon
b00a9ee938 Bisq Pagination and tx type select history. Increased transactions per page. 2020-08-10 23:47:44 +07:00
softsimon
d2a14e9cb7 Default to 1 sat/vB when mempool is empty. 2020-08-10 22:04:51 +07:00
softsimon
09b1a0d430 Hide "included in block" if block is clearly visible. 2020-08-10 17:48:07 +07:00
softsimon
1b9900ccf8 Add commit hash to the bottom of the about page
fixes #102
2020-08-10 14:59:29 +07:00
softsimon
cb51b71128 Display whole fee span on mined blocks.
fixes #101
2020-08-10 14:48:09 +07:00
softsimon
5411feee36 Default mempool backend to local electrs mock server. 2020-08-10 14:32:16 +07:00
softsimon
912a5dab27 Add proper timestamp to transaction pages. 2020-08-09 10:44:01 +07:00
softsimon
63fb733dc2 Adding OnPush to more components 2020-08-09 10:39:27 +07:00
softsimon
648be481d7 Upgrading bisq transaction type filter with a multiselect dropdown 2020-08-08 22:48:18 +07:00
softsimon
fea79f2ff4 Add pagination history to bisq transactions and blocks page. 2020-08-07 13:11:55 +07:00
softsimon
01b3407a9c Updated fee texts 2020-08-04 00:56:01 +07:00
softsimon
58ec9444a5 Change detection refactoring. 2020-08-02 19:20:38 +07:00
softsimon
bf1101ff66 Add testnet and bisq to local development proxy config. 2020-08-02 18:31:47 +07:00
softsimon
e6fa274aca Changedetection performance improvements. 2020-08-02 16:00:08 +07:00
softsimon
fa2a995de6 Fix for text overflow in footer 2020-08-02 15:30:28 +07:00
softsimon
7443522ed8 Merge pull request #100 from mempool/dependabot/npm_and_yarn/frontend/elliptic-6.5.3
Bump elliptic from 6.5.2 to 6.5.3 in /frontend
2020-08-01 12:35:37 +07:00
dependabot[bot]
dbe5f3bf06 Bump elliptic from 6.5.2 to 6.5.3 in /frontend
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-01 03:52:27 +00:00
softsimon
1f22819e0c Fixed margins. 2020-07-30 21:55:46 +07:00
softsimon
58a6bbd88b Subscription refactoring to increase performance. 2020-07-30 17:01:13 +07:00
softsimon
bb6272469d Refactor to use OnPush 2020-07-30 15:38:55 +07:00
softsimon
c1caaa37aa Refactor footer component to using OnPush. 2020-07-30 13:50:21 +07:00
softsimon
d1c786e2f6 Fee box: Only display on mainnet 2020-07-30 13:24:03 +07:00
softsimon
67dbea3faf Fee box fixes. 2020-07-30 13:13:22 +07:00
softsimon
bc92fb669b Display only the mempool blocks on mobile devices. 2020-07-30 12:43:22 +07:00
softsimon
51bed8e852 Merge pull request #99 from mempool/fees-box
Adding fee estimation component to front page
2020-07-29 21:54:46 +07:00
softsimon
81d7072a95 Fee box: Mobile view 2020-07-29 17:30:30 +07:00
softsimon
36d952b503 Fee block: Fees order fix. 2020-07-29 16:54:08 +07:00
softsimon
266c347292 Fee box: Handle empty mempool. 2020-07-29 15:20:23 +07:00
softsimon
9e0097e7b6 Adding fee estimation component to front page
fixes #98
2020-07-29 15:16:09 +07:00
softsimon
599159ecf0 Fix: Don't build statistics cache when DB is disabled. 2020-07-29 13:57:49 +07:00
softsimon
4c5ff7714e Adding eof new line 2020-07-28 14:27:38 +07:00
softsimon
010aef1e90 Bisq: burntFee should be displayed on asset listing fee tx types 2020-07-28 13:31:25 +07:00
267 changed files with 125515 additions and 31363 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: ['mempool'] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: ['https://mempool.space/about'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

77
.github/workflows/on-tag.yml vendored Normal file
View File

@@ -0,0 +1,77 @@
name: Docker build on tag
env:
DOCKER_CLI_EXPERIMENTAL: enabled
TAG_FMT: '^refs/tags/(((.?[0-9]+){3,4}))$'
DOCKER_BUILDKIT: 0
COMPOSE_DOCKER_CLI_BUILD: 0
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+
- v[0-9]+.[0-9]+.[0-9]+-*
jobs:
build:
strategy:
matrix:
service:
- frontend
- backend
runs-on: ubuntu-18.04
name: Build and push to DockerHub
steps:
- name: Set env variables
run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
- name: Show set environment variables
run: |
printf " TAG: %s\n" "$TAG"
- name: Login to Docker for building
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Checkout project
uses: actions/checkout@v2
- name: Init repo for Dockerization
run: docker/init.sh "$TAG"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
id: qemu
- name: Setup Docker buildx action
uses: docker/setup-buildx-action@v1
id: buildx
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Cache Docker layers
uses: actions/cache@v2
id: cache
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Run Docker buildx for ${{ matrix.service }} against tag
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \
--output "type=registry" ./${{ matrix.service }}/
- name: Run Docker buildx for ${{ matrix.service }} against latest
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \
--output "type=registry" ./${{ matrix.service }}/

View File

@@ -1,62 +0,0 @@
FROM alpine:latest
RUN mkdir /mempool.space/
COPY ./backend /mempool.space/backend/
COPY ./frontend /mempool.space/frontend/
COPY ./mariadb-structure.sql /mempool.space/mariadb-structure.sql
#COPY ./nginx.conf /mempool.space/nginx.conf
RUN apk add mariadb mariadb-client jq git nginx npm rsync
RUN mysql_install_db --user=mysql --datadir=/var/lib/mysql/
RUN /usr/bin/mysqld_safe --datadir='/var/lib/mysql/'& \
sleep 60 && \
mysql -e "create database mempool" && \
mysql -e "grant all privileges on mempool.* to 'mempool'@'localhost' identified by 'mempool'" && \
mysql mempool < /mempool.space/mariadb-structure.sql
RUN sed -i "/^skip-networking/ c#skip-networking" /etc/my.cnf.d/mariadb-server.cnf
RUN export NG_CLI_ANALYTICS=ci && \
npm install -g typescript && \
cd /mempool.space/frontend && \
npm install && \
cd /mempool.space/backend && \
npm install && \
tsc
COPY ./nginx-nossl-docker.conf /etc/nginx/nginx.conf
ENV ENV dev
ENV DB_HOST localhost
ENV DB_PORT 3306
ENV DB_USER mempool
ENV DB_PASSWORD mempool
ENV DB_DATABASE mempool
ENV HTTP_PORT 80
ENV API_ENDPOINT /api/v1/
ENV CHAT_SSL_ENABLED false
#ENV CHAT_SSL_PRIVKEY
#ENV CHAT_SSL_CERT
#ENV CHAT_SSL_CHAIN
ENV MEMPOOL_REFRESH_RATE_MS 500
ENV INITIAL_BLOCK_AMOUNT 8
ENV DEFAULT_PROJECTED_BLOCKS_AMOUNT 8
ENV KEEP_BLOCK_AMOUNT 24
ENV BITCOIN_NODE_HOST bitcoinhost
ENV BITCOIN_NODE_PORT 8332
ENV BITCOIN_NODE_USER bitcoinuser
ENV BITCOIN_NODE_PASS bitcoinpass
ENV TX_PER_SECOND_SPAN_SECONDS 150
#RUN echo "mysqld_safe& sleep 20 && cd /mempool.space/backend && rm -f mempool-config.json && rm -f cache.json && touch cache.json && jq -n env > mempool-config.json && node dist/index.js" > /entrypoint.sh
RUN cd /mempool.space/frontend/ && \
npm run build && \
rsync -av --delete dist/mempool/ /var/www/html/
EXPOSE 80
COPY ./entrypoint.sh /mempool.space/entrypoint.sh
RUN chmod +x /mempool.space/entrypoint.sh
WORKDIR /mempool.space
CMD ["/mempool.space/entrypoint.sh"]

20
LICENSE
View File

@@ -1,11 +1,11 @@
MIT License
MIT License with Commons Clause License Condition v1.0
Copyright (c) 2019 Simon Lindh
Copyright (c) 2019-2020 The Mempool Open Source Project
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
to use, copy, modify, merge, publish, distribute, and/or sublicense
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
@@ -19,3 +19,17 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Commons Clause License Condition v1.0
Without limiting other conditions in the License, the grant of rights under
the License will not include, and the License does not grant to you, the
right to Sell the Software.
For purposes of the foregoing, “Sell” means practicing any or all of the
rights granted to you under the License to provide to third parties, for a
fee or other consideration (including without limitation fees for hosting or
consulting/ support services related to the Software), a product or service
whose value derives, entirely or substantially, from the functionality of
the Software. Any license notice or attribution required by the License must
also include this Commons Cause License Condition notice.

223
README.md
View File

@@ -1,92 +1,53 @@
# mempool
## a mempool visualizer and explorer for Bitcoin
# The Mempool Open Source Project
![mempool](https://pbs.twimg.com/media/EAETXWCU4AAv2v-?format=jpg&name=4096x4096)
![blockchain](https://pbs.twimg.com/media/EAETXWAU8AAj4IP?format=jpg&name=4096x4096)
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.
## Pick the right version for your use case
![mempool](https://mempool.space/resources/screenshots/v2.1.0-dashboard.png)
Mempool V1 has basic explorer functionality and can run from a Bitcoin Core full node on a Raspberry Pi (no pruning, txindex=1).
## Installation Methods
Mempool V2 is what runs on https://mempool.space and has advanced explorer functionality, but requires a fully synced electrs backend running on powerful server hardware.
Mempool can be self-hosted on a wide variety of your own hardware, ranging from a simple one-click installation on a Raspberry Pi distro, all the way to an advanced high availability cluster of powerful servers for a production instance. We support the following installation methods, ranked in order from simple to advanced:
# Mempool V1 using Docker (easy)
1) One-click installation on: [Umbrel](https://github.com/getumbrel/umbrel), [RaspiBlitz](https://github.com/rootzoll/raspiblitz), [RoninDojo](https://code.samourai.io/ronindojo/RoninDojo), or [MyNode](https://github.com/mynodebtc/mynode).
2) [Docker installation on Linux using docker-compose](https://github.com/mempool/mempool/tree/master/docker)
3) [Manual installation on Linux or FreeBSD](https://github.com/mempool/mempool#manual-installation)
4) [Production installation on a powerful FreeBSD server](https://github.com/mempool/mempool/tree/master/production)
5) [High Availability cluster using powerful FreeBSD servers](https://github.com/mempool/mempool/tree/master/production#high-availability)
Install from Docker Hub, passing your Bitcoin Core RPC credentials as environment variables:
# Manual Installation
```bash
docker pull mempool/mempool:v1.0
docker create -p 80:80 -e BITCOIN_NODE_HOST=192.168.1.102 -e BITCOIN_NODE_USER=foo -e BITCOIN_NODE_PASS=bar --name mempool mempool/mempool:v1.0
docker start mempool
docker logs mempool
```
You should see mempool starting up, which takes over an hour (needs 8 blocks). When it's ready, visit http://127.0.0.1/ to see your mempool.
# Mempool V1 not using Docker (advanced)
The following instructions are for a manual installation on Linux or FreeBSD. The file and directory paths may need to be changed to match your OS.
## Dependencies
* Bitcoin (full node required, no pruning, txindex=1)
* Bitcoin Core (no pruning, txindex=1)
* Electrum Server (romanz/electrs)
* NodeJS (official stable LTS)
* MySQL or MariaDB (default config)
* Nginx (use supplied nginx.conf)
* MariaDB (default config)
* Nginx (use supplied nginx.conf and nginx-mempool.conf)
## Checking out release tag
## Mempool
Clone the mempool repo, and checkout the latest release tag:
```bash
git clone https://github.com/mempool-space/mempool.space
cd mempool.space
git checkout v1.0.0 # put latest release tag here
git clone https://github.com/mempool/mempool
cd mempool
latestrelease=$(curl -s https://api.github.com/repos/mempool/mempool/releases/latest|grep tag_name|head -1|cut -d '"' -f4)
git checkout $latestrelease
```
## Bitcoin Core (bitcoind)
Enable RPC and txindex in bitcoin.conf
Enable RPC and txindex in `bitcoin.conf`:
```bash
rpcuser=mempool
rpcpassword=71b61986da5b03a5694d7c7d5165ece5
txindex=1
```
## NodeJS
Install dependencies and build code:
```bash
# Install TypeScript Globally
npm install -g typescript
# Frontend
cd frontend
npm install
npm run build
# Backend
cd ../backend/
npm install
npm run build
```
## Mempool Configuration
In the `backend` folder, make a copy of the sample config and modify it to fit your settings.
```bash
cp mempool-config.sample.json mempool-config.json
```
Edit `mempool-config.json` to add your Bitcoin Core node RPC credentials:
```bash
"BITCOIN_NODE_HOST": "192.168.1.5",
"BITCOIN_NODE_PORT": 8332,
"BITCOIN_NODE_USER": "mempool",
"BITCOIN_NODE_PASS": "71b61986da5b03a5694d7c7d5165ece5",
```
## MySQL
Install MariaDB:
Install MariaDB from OS package manager:
```bash
# Linux
apt-get install mariadb-server mariadb-client
@@ -104,51 +65,72 @@ Create database and grant privileges:
MariaDB [(none)]> create database mempool;
Query OK, 1 row affected (0.00 sec)
MariaDB [(none)]> grant all privileges on mempool.* to 'mempool' identified by 'mempool';
MariaDB [(none)]> grant all privileges on mempool.* to 'mempool'@'%' identified by 'mempool';
Query OK, 0 rows affected (0.00 sec)
```
From the root folder, initialize database structure:
From the mempool repo's top-level folder, import the database structure:
```bash
mysql -u mempool -p mempool < mariadb-structure.sql
```
## Running (Backend)
Create an initial empty cache and start the app:
## Mempool Backend
Install mempool dependencies from npm and build the backend:
```bash
touch cache.json
npm run start # node dist/index.js
# backend
cd ../backend/
npm install
npm run build
```
After starting you should see:
In the `backend` folder, make a copy of the sample config and modify it to fit your settings.
```bash
Server started on port 8999 :)
New block found (#586498)! 0 of 1986 found in mempool. 1985 not found.
New block found (#586499)! 0 of 1094 found in mempool. 1093 not found.
New block found (#586500)! 0 of 2735 found in mempool. 2734 not found.
New block found (#586501)! 0 of 2675 found in mempool. 2674 not found.
New block found (#586502)! 0 of 975 found in mempool. 974 not found.
New block found (#586503)! 0 of 2130 found in mempool. 2129 not found.
New block found (#586504)! 0 of 2770 found in mempool. 2769 not found.
New block found (#586505)! 0 of 2759 found in mempool. 2758 not found.
Updating mempool
Calculated fee for transaction 1 / 3257
Calculated fee for transaction 2 / 3257
Calculated fee for transaction 3 / 3257
Calculated fee for transaction 4 / 3257
Calculated fee for transaction 5 / 3257
Calculated fee for transaction 6 / 3257
Calculated fee for transaction 7 / 3257
Calculated fee for transaction 8 / 3257
Calculated fee for transaction 9 / 3257
cp mempool-config.sample.json mempool-config.json
```
You need to wait for at least *8 blocks to be mined*, so please wait ~80 minutes.
The backend also needs to index transactions, calculate fees, etc.
When it's ready you will see output like this:
Edit `mempool-config.json` to add your Bitcoin Core node RPC credentials:
```bash
{
"MEMPOOL": {
"NETWORK": "mainnet",
"BACKEND": "electrum",
"HTTP_PORT": 8999,
"API_URL_PREFIX": "/api/v1/",
"POLL_RATE_MS": 2000
},
"CORE_RPC": {
"USERNAME": "mempool",
"PASSWORD": "71b61986da5b03a5694d7c7d5165ece5"
},
"ELECTRUM": {
"HOST": "127.0.0.1",
"PORT": 50002,
"TLS_ENABLED": true,
},
"DATABASE": {
"ENABLED": true,
"HOST": "127.0.0.1",
"PORT": 3306,
"USERNAME": "mempool",
"PASSWORD": "mempool",
"DATABASE": "mempool"
},
"STATISTICS": {
"ENABLED": true,
"TX_PER_SECOND_SAMPLE_PERIOD": 150
}
}
```
Start the backend:
```bash
npm run start
```
When it's running you should see output like this:
```bash
Mempool updated in 0.189 seconds
@@ -171,43 +153,38 @@ When it's ready you will see output like this:
Updating mempool
```
## nginx + CertBot (LetsEncrypt)
Setup nginx using the supplied nginx.conf
## Mempool Frontend
Install mempool dependencies from npm and build the frontend static HTML/CSS/JS:
```bash
# frontend
cd frontend
npm install
npm run build
```
Install the output into nginx webroot folder:
```bash
sudo rsync -av --delete dist/mempool/ /var/www/html/
```
## nginx + certbot
Install the supplied nginx.conf and nginx-mempool.conf in /etc/nginx
```bash
# install nginx and certbot
apt-get install -y nginx python-certbot-nginx
# install the mempool configuration for nginx
cp nginx.conf nginx-mempool.conf /etc/nginx/nginx.conf
# replace example.com with your domain name
certbot --nginx -d example.com
# install the mempool configuration for nginx
cp nginx.conf /etc/nginx/nginx.conf
# edit the installed nginx.conf, and replace all
# instances of example.com with your domain name
```
Make sure you can access https://<your-domain-name>/ in browser before proceeding
## Running (Frontend)
Build the frontend static HTML/CSS/JS, rsync the output into nginx folder:
```bash
cd frontend/
npm run build
sudo rsync -av --delete dist/mempool/ /var/www/html/
```
### Optional frontend configuration
In the `frontend` folder, make a copy of the sample config and modify it to fit your settings.
```bash
cp mempool-frontend-config.sample.json mempool-frontend-config.json
```
## Try It Out
If everything went okay you should see the beautiful mempool :grin:

2
backend/.gitignore vendored
View File

@@ -41,5 +41,3 @@ testem.log
#System Files
.DS_Store
Thumbs.db
cache.json

1
backend/cache/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.json

View File

@@ -1,22 +1,53 @@
{
"HTTP_PORT": 8999,
"DB_HOST": "localhost",
"DB_PORT": 3306,
"DB_USER": "mempool",
"DB_PASSWORD": "mempool",
"DB_DATABASE": "mempool",
"DB_DISABLED": false,
"API_ENDPOINT": "/api/v1/",
"ELECTRS_POLL_RATE_MS": 2000,
"MEMPOOL_REFRESH_RATE_MS": 2000,
"DEFAULT_PROJECTED_BLOCKS_AMOUNT": 8,
"KEEP_BLOCK_AMOUNT": 24,
"INITIAL_BLOCK_AMOUNT": 8,
"TX_PER_SECOND_SPAN_SECONDS": 150,
"ELECTRS_API_URL": "https://www.blockstream.info/testnet/api",
"BISQ_ENABLED": false,
"BSQ_BLOCKS_DATA_PATH": "/bisq/data",
"SSL": false,
"SSL_CERT_FILE_PATH": "/etc/letsencrypt/live/mysite/fullchain.pem",
"SSL_KEY_FILE_PATH": "/etc/letsencrypt/live/mysite/privkey.pem"
"MEMPOOL": {
"NETWORK": "mainnet",
"BACKEND": "electrum",
"HTTP_PORT": 8999,
"SPAWN_CLUSTER_PROCS": 0,
"API_URL_PREFIX": "/api/v1/",
"POLL_RATE_MS": 2000,
"CACHE_DIR": "./cache",
"CLEAR_PROTECTION_MINUTES": 20
},
"CORE_RPC": {
"HOST": "127.0.0.1",
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool"
},
"ELECTRUM": {
"HOST": "127.0.0.1",
"PORT": 50002,
"TLS_ENABLED": true
},
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:3000"
},
"CORE_RPC_MINFEE": {
"ENABLED": false,
"HOST": "127.0.0.1",
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool"
},
"DATABASE": {
"ENABLED": true,
"HOST": "127.0.0.1",
"PORT": 3306,
"DATABASE": "mempool",
"USERNAME": "mempool",
"PASSWORD": "mempool"
},
"STATISTICS": {
"ENABLED": true,
"TX_PER_SECOND_SAMPLE_PERIOD": 150
},
"BISQ_BLOCKS": {
"ENABLED": false,
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db/json"
},
"BISQ_MARKETS": {
"ENABLED": false,
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
}
}

4109
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "mempool-backend",
"version": "2.0.0",
"version": "2.1.2",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "MIT",
"homepage": "https://mempool.space",
@@ -20,21 +20,29 @@
],
"main": "index.ts",
"scripts": {
"build": "tsc",
"start": "npm run build && node dist/index.js",
"ng": "./node_modules/@angular/cli/bin/ng",
"tsc": "./node_modules/typescript/bin/tsc",
"build": "npm run tsc",
"start": "node --max-old-space-size=2048 dist/index.js",
"start-production": "node --max-old-space-size=4096 dist/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"compression": "^1.7.4",
"@mempool/bitcoin": "^3.0.2",
"@mempool/electrum-client": "^1.1.7",
"axios": "^0.21.1",
"bitcoinjs-lib": "^5.2.0",
"crypto-js": "^4.0.0",
"express": "^4.17.1",
"mysql2": "^1.6.1",
"request": "^2.88.2",
"locutus": "^2.0.12",
"mysql2": "2.2.5",
"node-worker-threads-pool": "^1.4.2",
"ws": "^7.3.1"
},
"devDependencies": {
"@types/compression": "^1.0.1",
"@types/express": "^4.17.2",
"@types/request": "^2.48.2",
"@types/locutus": "^0.0.6",
"@types/ws": "^6.0.4",
"tslint": "~6.1.0",
"typescript": "~3.9.7"

View File

@@ -1,5 +1,6 @@
import * as fs from 'fs';
import * as os from 'os';
import logger from '../logger';
class BackendInfo {
gitCommitHash = '';
@@ -17,11 +18,15 @@ class BackendInfo {
};
}
public getShortCommitHash() {
return this.gitCommitHash.slice(0, 7);
}
private setLatestCommitHash(): void {
try {
this.gitCommitHash = fs.readFileSync('../.git/refs/heads/master').toString().trim();
} catch (e) {
console.log('Could not load git commit info, skipping.');
logger.err('Could not load git commit info: ' + e.message || e);
}
}
}

View File

@@ -1,11 +1,14 @@
const config = require('../../mempool-config.json');
import config from '../../config';
import * as fs from 'fs';
import * as request from 'request';
import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from '../interfaces';
import { Common } from './common';
import axios from 'axios';
import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from './interfaces';
import { Common } from '../common';
import { BlockExtended } from '../../mempool.interfaces';
import { StaticPool } from 'node-worker-threads-pool';
import logger from '../../logger';
class Bisq {
private static BLOCKS_JSON_FILE_PATH = '/all/blocks.json';
private static BLOCKS_JSON_FILE_PATH = config.BISQ_BLOCKS.DATA_PATH + '/all/blocks.json';
private latestBlockHeight = 0;
private blocks: BisqBlock[] = [];
private transactions: BisqTransaction[] = [];
@@ -23,6 +26,10 @@ class Bisq {
private priceUpdateCallbackFunction: ((price: number) => void) | undefined;
private topDirectoryWatcher: fs.FSWatcher | undefined;
private subdirectoryWatcher: fs.FSWatcher | undefined;
private jsonParsePool = new StaticPool({
size: 4,
task: (blob: string) => JSON.parse(blob),
});
constructor() {}
@@ -35,6 +42,14 @@ class Bisq {
this.startSubDirectoryWatcher();
}
handleNewBitcoinBlock(block: BlockExtended): void {
if (block.height - 10 > this.latestBlockHeight && this.latestBlockHeight !== 0) {
logger.warn(`Bitcoin block height (#${block.height}) has diverged from the latest Bisq block height (#${this.latestBlockHeight}). Restarting watchers...`);
this.startTopDirectoryWatcher();
this.startSubDirectoryWatcher();
}
}
getTransaction(txId: string): BisqTransaction | undefined {
return this.transactionIndex[txId];
}
@@ -72,8 +87,8 @@ class Bisq {
}
private checkForBisqDataFolder() {
if (!fs.existsSync(config.BSQ_BLOCKS_DATA_PATH + Bisq.BLOCKS_JSON_FILE_PATH)) {
console.log(config.BSQ_BLOCKS_DATA_PATH + Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist. Make sure Bisq is running and the config is correct before starting the server.`);
if (!fs.existsSync(Bisq.BLOCKS_JSON_FILE_PATH)) {
logger.warn(Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist. Make sure Bisq is running and the config is correct before starting the server.`);
return process.exit(1);
}
}
@@ -83,7 +98,7 @@ class Bisq {
this.topDirectoryWatcher.close();
}
let fsWait: NodeJS.Timeout | null = null;
this.topDirectoryWatcher = fs.watch(config.BSQ_BLOCKS_DATA_PATH, () => {
this.topDirectoryWatcher = fs.watch(config.BISQ_BLOCKS.DATA_PATH, () => {
if (fsWait) {
clearTimeout(fsWait);
}
@@ -91,7 +106,7 @@ class Bisq {
this.subdirectoryWatcher.close();
}
fsWait = setTimeout(() => {
console.log(`Bisq restart detected. Resetting both watchers in 3 minutes.`);
logger.debug(`Bisq restart detected. Resetting both watchers in 3 minutes.`);
setTimeout(() => {
this.startTopDirectoryWatcher();
this.startSubDirectoryWatcher();
@@ -105,36 +120,37 @@ class Bisq {
if (this.subdirectoryWatcher) {
this.subdirectoryWatcher.close();
}
if (!fs.existsSync(config.BSQ_BLOCKS_DATA_PATH + Bisq.BLOCKS_JSON_FILE_PATH)) {
console.log(config.BSQ_BLOCKS_DATA_PATH + Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist. Trying to restart sub directory watcher again in 3 minutes.`);
if (!fs.existsSync(Bisq.BLOCKS_JSON_FILE_PATH)) {
logger.warn(Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist. Trying to restart sub directory watcher again in 3 minutes.`);
setTimeout(() => this.startSubDirectoryWatcher(), 180000);
return;
}
let fsWait: NodeJS.Timeout | null = null;
this.subdirectoryWatcher = fs.watch(config.BSQ_BLOCKS_DATA_PATH + '/all', () => {
this.subdirectoryWatcher = fs.watch(config.BISQ_BLOCKS.DATA_PATH + '/all', () => {
if (fsWait) {
clearTimeout(fsWait);
}
fsWait = setTimeout(() => {
console.log(`Change detected in the Bisq data folder.`);
logger.debug(`Change detected in the Bisq data folder.`);
this.loadBisqDumpFile();
}, 2000);
});
}
private updatePrice() {
request('https://markets.bisq.network/api/trades/?market=bsq_btc', { json: true }, (err, res, trades: BisqTrade[]) => {
if (err) { return console.log(err); }
const prices: number[] = [];
trades.forEach((trade) => {
prices.push(parseFloat(trade.price) * 100000000);
});
prices.sort((a, b) => a - b);
this.price = Common.median(prices);
if (this.priceUpdateCallbackFunction) {
this.priceUpdateCallbackFunction(this.price);
}
axios.get<BisqTrade[]>('https://bisq.markets/api/trades/?market=bsq_btc', { timeout: 10000 })
.then((response) => {
const prices: number[] = [];
response.data.forEach((trade) => {
prices.push(parseFloat(trade.price) * 100000000);
});
prices.sort((a, b) => a - b);
this.price = Common.median(prices);
if (this.priceUpdateCallbackFunction) {
this.priceUpdateCallbackFunction(this.price);
}
}).catch((err) => {
logger.err('Error updating Bisq market price: ' + err);
});
}
@@ -145,7 +161,7 @@ class Bisq {
this.buildIndex();
this.calculateStats();
} catch (e) {
console.log('loadBisqDumpFile() error.', e.message);
logger.err('loadBisqDumpFile() error.' + e.message || e);
}
}
@@ -189,7 +205,7 @@ class Bisq {
});
const time = new Date().getTime() - start;
console.log('Bisq data index rebuilt in ' + time + ' ms');
logger.debug('Bisq data index rebuilt in ' + time + ' ms');
}
private calculateStats() {
@@ -217,8 +233,8 @@ class Bisq {
this.stats = {
addresses: Object.keys(this.addressIndex).length,
minted: minted,
burnt: burned,
minted: minted / 100,
burnt: burned / 100,
spent_txos: spent,
unspent_txos: unspent,
};
@@ -227,14 +243,14 @@ class Bisq {
private async loadBisqBlocksDump(cacheData: string): Promise<void> {
const start = new Date().getTime();
if (cacheData && cacheData.length !== 0) {
console.log('Processing Bisq data dump...');
const data: BisqBlocks = JSON.parse(cacheData);
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();
this.latestBlockHeight = data.chainHeight;
const time = new Date().getTime() - start;
console.log('Bisq dump processed in ' + time + ' ms');
logger.debug('Bisq dump processed in ' + time + ' ms (worker thread)');
} else {
throw new Error(`Bisq dump didn't contain any blocks`);
}
@@ -243,10 +259,10 @@ class Bisq {
private loadData(): Promise<string> {
return new Promise((resolve, reject) => {
if (!fs.existsSync(config.BSQ_BLOCKS_DATA_PATH + Bisq.BLOCKS_JSON_FILE_PATH)) {
if (!fs.existsSync(Bisq.BLOCKS_JSON_FILE_PATH)) {
return reject(Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist`);
}
fs.readFile(config.BSQ_BLOCKS_DATA_PATH + Bisq.BLOCKS_JSON_FILE_PATH, 'utf8', (err, data) => {
fs.readFile(Bisq.BLOCKS_JSON_FILE_PATH, 'utf8', (err, data) => {
if (err) {
reject(err);
}

View File

@@ -0,0 +1,258 @@
export interface BisqBlocks {
chainHeight: number;
blocks: BisqBlock[];
}
export interface BisqBlock {
height: number;
time: number;
hash: string;
previousBlockHash: string;
txs: BisqTransaction[];
}
export interface BisqTransaction {
txVersion: string;
id: string;
blockHeight: number;
blockHash: string;
time: number;
inputs: BisqInput[];
outputs: BisqOutput[];
txType: string;
txTypeDisplayString: string;
burntFee: number;
invalidatedBsq: number;
unlockBlockHeight: number;
}
export interface BisqStats {
minted: number;
burnt: number;
addresses: number;
unspent_txos: number;
spent_txos: number;
}
interface BisqInput {
spendingTxOutputIndex: number;
spendingTxId: string;
bsqAmount: number;
isVerified: boolean;
address: string;
time: number;
}
interface BisqOutput {
txVersion: string;
txId: string;
index: number;
bsqAmount: number;
btcAmount: number;
height: number;
isVerified: boolean;
burntFee: number;
invalidatedBsq: number;
address: string;
scriptPubKey: BisqScriptPubKey;
time: any;
txType: string;
txTypeDisplayString: string;
txOutputType: string;
txOutputTypeDisplayString: string;
lockTime: number;
isUnspent: boolean;
spentInfo: SpentInfo;
opReturn?: string;
}
interface BisqScriptPubKey {
addresses: string[];
asm: string;
hex: string;
reqSigs: number;
type: string;
}
interface SpentInfo {
height: number;
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; }
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 {
direction: string;
price: string;
amount: string;
volume: string;
payment_method: string;
trade_id: string;
trade_date: number;
}
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;
}

View File

@@ -0,0 +1,655 @@
import { Currencies, OffersData, TradesData, Depth, Currency, Interval, HighLowOpenClose,
Markets, Offers, Offer, BisqTrade, MarketVolume, Tickers, Ticker, SummarizedIntervals, SummarizedInterval } from './interfaces';
import * as datetime from 'locutus/php/datetime';
class BisqMarketsApi {
private cryptoCurrencyData: Currency[] = [];
private fiatCurrencyData: Currency[] = [];
private activeCryptoCurrencyData: Currency[] = [];
private activeFiatCurrencyData: Currency[] = [];
private offersData: OffersData[] = [];
private tradesData: TradesData[] = [];
private fiatCurrenciesIndexed: { [code: string]: true } = {};
private allCurrenciesIndexed: { [code: string]: Currency } = {};
private tradeDataByMarket: { [market: string]: TradesData[] } = {};
private tickersCache: Ticker | Tickers | null = null;
constructor() { }
setOffersData(offers: OffersData[]) {
this.offersData = offers;
}
setTradesData(trades: TradesData[]) {
this.tradesData = trades;
this.tradeDataByMarket = {};
this.tradesData.forEach((trade) => {
trade._market = trade.currencyPair.toLowerCase().replace('/', '_');
if (!this.tradeDataByMarket[trade._market]) {
this.tradeDataByMarket[trade._market] = [];
}
this.tradeDataByMarket[trade._market].push(trade);
});
}
setCurrencyData(cryptoCurrency: Currency[], fiatCurrency: Currency[], activeCryptoCurrency: Currency[], activeFiatCurrency: Currency[]) {
this.cryptoCurrencyData = cryptoCurrency,
this.fiatCurrencyData = fiatCurrency,
this.activeCryptoCurrencyData = activeCryptoCurrency,
this.activeFiatCurrencyData = activeFiatCurrency;
this.fiatCurrenciesIndexed = {};
this.allCurrenciesIndexed = {};
this.fiatCurrencyData.forEach((currency) => {
currency._type = 'fiat';
this.fiatCurrenciesIndexed[currency.code] = true;
this.allCurrenciesIndexed[currency.code] = currency;
});
this.cryptoCurrencyData.forEach((currency) => {
currency._type = 'crypto';
this.allCurrenciesIndexed[currency.code] = currency;
});
}
updateCache() {
this.tickersCache = null;
this.tickersCache = this.getTicker();
}
getCurrencies(
type: 'crypto' | 'fiat' | 'active' | 'all' = 'all',
): Currencies {
let currencies: Currency[];
switch (type) {
case 'fiat':
currencies = this.fiatCurrencyData;
break;
case 'crypto':
currencies = this.cryptoCurrencyData;
break;
case 'active':
currencies = this.activeCryptoCurrencyData.concat(this.activeFiatCurrencyData);
break;
case 'all':
default:
currencies = this.cryptoCurrencyData.concat(this.fiatCurrencyData);
}
const result = {};
currencies.forEach((currency) => {
result[currency.code] = currency;
});
return result;
}
getDepth(
market: string,
): Depth {
const currencyPair = market.replace('_', '/').toUpperCase();
const buys = this.offersData
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'BUY')
.map((offer) => offer.price)
.sort((a, b) => b - a)
.map((price) => this.intToBtc(price));
const sells = this.offersData
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'SELL')
.map((offer) => offer.price)
.sort((a, b) => a - b)
.map((price) => this.intToBtc(price));
const result = {};
result[market] = {
'buys': buys,
'sells': sells,
};
return result;
}
getOffers(
market: string,
direction?: 'buy' | 'sell',
): Offers {
const currencyPair = market.replace('_', '/').toUpperCase();
let buys: Offer[] | null = null;
let sells: Offer[] | null = null;
if (!direction || direction === 'buy') {
buys = this.offersData
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'BUY')
.sort((a, b) => b.price - a.price)
.map((offer) => this.offerDataToOffer(offer, market));
}
if (!direction || direction === 'sell') {
sells = this.offersData
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'SELL')
.sort((a, b) => a.price - b.price)
.map((offer) => this.offerDataToOffer(offer, market));
}
const result: Offers = {};
result[market] = {
'buys': buys,
'sells': sells,
};
return result;
}
getMarkets(): Markets {
const allCurrencies = this.getCurrencies();
const activeCurrencies = this.getCurrencies('active');
const markets = {};
for (const currency of Object.keys(activeCurrencies)) {
if (allCurrencies[currency].code === 'BTC') {
continue;
}
const isFiat = allCurrencies[currency]._type === 'fiat';
const pmarketname = allCurrencies['BTC']['name'];
const lsymbol = isFiat ? 'BTC' : currency;
const rsymbol = isFiat ? currency : 'BTC';
const lname = isFiat ? pmarketname : allCurrencies[currency].name;
const rname = isFiat ? allCurrencies[currency].name : pmarketname;
const ltype = isFiat ? 'crypto' : allCurrencies[currency]._type;
const rtype = isFiat ? 'fiat' : 'crypto';
const lprecision = 8;
const rprecision = isFiat ? 2 : 8;
const pair = lsymbol.toLowerCase() + '_' + rsymbol.toLowerCase();
markets[pair] = {
'pair': pair,
'lname': lname,
'rname': rname,
'lsymbol': lsymbol,
'rsymbol': rsymbol,
'lprecision': lprecision,
'rprecision': rprecision,
'ltype': ltype,
'rtype': rtype,
'name': lname + '/' + rname,
};
}
return markets;
}
getTrades(
market: string,
timestamp_from?: number,
timestamp_to?: number,
trade_id_from?: string,
trade_id_to?: string,
direction?: 'buy' | 'sell',
limit: number = 100,
sort: 'asc' | 'desc' = 'desc',
): BisqTrade[] {
limit = Math.min(limit, 2000);
const _market = market === 'all' ? undefined : market;
if (!timestamp_from) {
timestamp_from = new Date('2016-01-01').getTime() / 1000;
}
if (!timestamp_to) {
timestamp_to = new Date().getTime() / 1000;
}
const matches = this.getTradesByCriteria(_market, timestamp_to, timestamp_from,
trade_id_to, trade_id_from, direction, sort, limit, false);
if (sort === 'asc') {
matches.sort((a, b) => a.tradeDate - b.tradeDate);
} else {
matches.sort((a, b) => b.tradeDate - a.tradeDate);
}
return matches.map((trade) => {
const bsqTrade: BisqTrade = {
direction: trade.primaryMarketDirection,
price: trade._tradePriceStr,
amount: trade._tradeAmountStr,
volume: trade._tradeVolumeStr,
payment_method: trade.paymentMethod,
trade_id: trade.offerId,
trade_date: trade.tradeDate,
};
if (market === 'all') {
bsqTrade.market = trade._market;
}
return bsqTrade;
});
}
getVolumes(
market?: string,
timestamp_from?: number,
timestamp_to?: number,
interval: Interval = 'auto',
milliseconds?: boolean,
timestamp: 'no' | 'yes' = 'yes',
): MarketVolume[] {
if (milliseconds) {
timestamp_from = timestamp_from ? timestamp_from / 1000 : timestamp_from;
timestamp_to = timestamp_to ? timestamp_to / 1000 : timestamp_to;
}
if (!timestamp_from) {
timestamp_from = new Date('2016-01-01').getTime() / 1000;
}
if (!timestamp_to) {
timestamp_to = new Date().getTime() / 1000;
}
const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from,
undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER);
if (interval === 'auto') {
const range = timestamp_to - timestamp_from;
interval = this.getIntervalFromRange(range);
}
const intervals: any = {};
const marketVolumes: MarketVolume[] = [];
for (const trade of trades) {
const traded_at = trade['tradeDate'] / 1000;
const interval_start = this.intervalStart(traded_at, interval);
if (!intervals[interval_start]) {
intervals[interval_start] = {
'volume': 0,
'num_trades': 0,
};
}
const period = intervals[interval_start];
period['period_start'] = interval_start;
period['volume'] += this.fiatCurrenciesIndexed[trade.currency] ? trade._tradeAmount : trade._tradeVolume;
period['num_trades']++;
}
for (const p in intervals) {
if (intervals.hasOwnProperty(p)) {
const period = intervals[p];
marketVolumes.push({
period_start: timestamp === 'no' ? new Date(period['period_start'] * 1000).toISOString() : period['period_start'],
num_trades: period['num_trades'],
volume: this.intToBtc(period['volume']),
});
}
}
return marketVolumes;
}
getTicker(
market?: string,
): Tickers | Ticker | null {
if (market) {
return this.getTickerFromMarket(market);
}
if (this.tickersCache) {
return this.tickersCache;
}
const allMarkets = this.getMarkets();
const tickers = {};
for (const m in allMarkets) {
if (allMarkets.hasOwnProperty(m)) {
tickers[allMarkets[m].pair] = this.getTickerFromMarket(allMarkets[m].pair);
}
}
return tickers;
}
getTickerFromMarket(market: string): Ticker | null {
let ticker: Ticker;
const timestamp_from = datetime.strtotime('-24 hour');
const timestamp_to = new Date().getTime() / 1000;
const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from,
undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER);
const periods: SummarizedInterval[] = Object.values(this.getTradesSummarized(trades, timestamp_from));
const allCurrencies = this.getCurrencies();
const currencyRight = allCurrencies[market.split('_')[1].toUpperCase()];
if (periods[0]) {
ticker = {
'last': this.intToBtc(periods[0].close),
'high': this.intToBtc(periods[0].high),
'low': this.intToBtc(periods[0].low),
'volume_left': this.intToBtc(periods[0].volume_left),
'volume_right': this.intToBtc(periods[0].volume_right),
'buy': null,
'sell': null,
};
} else {
const lastTrade = this.tradeDataByMarket[market];
if (!lastTrade) {
return null;
}
const tradePrice = lastTrade[0].primaryMarketTradePrice * Math.pow(10, 8 - currencyRight.precision);
const lastTradePrice = this.intToBtc(tradePrice);
ticker = {
'last': lastTradePrice,
'high': lastTradePrice,
'low': lastTradePrice,
'volume_left': '0',
'volume_right': '0',
'buy': null,
'sell': null,
};
}
const timestampFromMilli = timestamp_from * 1000;
const timestampToMilli = timestamp_to * 1000;
const currencyPair = market.replace('_', '/').toUpperCase();
const offersData = this.offersData.slice().sort((a, b) => a.price - b.price);
const buy = offersData.find((offer) => offer.currencyPair === currencyPair
&& offer.primaryMarketDirection === 'BUY'
&& offer.date >= timestampFromMilli
&& offer.date <= timestampToMilli
);
const sell = offersData.find((offer) => offer.currencyPair === currencyPair
&& offer.primaryMarketDirection === 'SELL'
&& offer.date >= timestampFromMilli
&& offer.date <= timestampToMilli
);
if (buy) {
ticker.buy = this.intToBtc(buy.primaryMarketPrice * Math.pow(10, 8 - currencyRight.precision));
}
if (sell) {
ticker.sell = this.intToBtc(sell.primaryMarketPrice * Math.pow(10, 8 - currencyRight.precision));
}
return ticker;
}
getHloc(
market: string,
interval: Interval = 'auto',
timestamp_from?: number,
timestamp_to?: number,
milliseconds?: boolean,
timestamp: 'no' | 'yes' = 'yes',
): HighLowOpenClose[] {
if (milliseconds) {
timestamp_from = timestamp_from ? timestamp_from / 1000 : timestamp_from;
timestamp_to = timestamp_to ? timestamp_to / 1000 : timestamp_to;
}
if (!timestamp_from) {
timestamp_from = new Date('2016-01-01').getTime() / 1000;
}
if (!timestamp_to) {
timestamp_to = new Date().getTime() / 1000;
}
const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from,
undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER);
if (interval === 'auto') {
const range = timestamp_to - timestamp_from;
interval = this.getIntervalFromRange(range);
}
const intervals = this.getTradesSummarized(trades, timestamp_from, interval);
const hloc: HighLowOpenClose[] = [];
for (const p in intervals) {
if (intervals.hasOwnProperty(p)) {
const period = intervals[p];
hloc.push({
period_start: timestamp === 'no' ? new Date(period['period_start'] * 1000).toISOString() : period['period_start'],
open: this.intToBtc(period['open']),
close: this.intToBtc(period['close']),
high: this.intToBtc(period['high']),
low: this.intToBtc(period['low']),
avg: this.intToBtc(period['avg']),
volume_right: this.intToBtc(period['volume_right']),
volume_left: this.intToBtc(period['volume_left']),
});
}
}
return hloc;
}
private getIntervalFromRange(range: number): Interval {
// two days range loads minute data
if (range <= 3600) {
// up to one hour range loads minutely data
return 'minute';
} else if (range <= 1 * 24 * 3600) {
// up to one day range loads half-hourly data
return 'half_hour';
} else if (range <= 3 * 24 * 3600) {
// up to 3 day range loads hourly data
return 'hour';
} else if (range <= 7 * 24 * 3600) {
// up to 7 day range loads half-daily data
return 'half_day';
} else if (range <= 60 * 24 * 3600) {
// up to 2 month range loads daily data
return 'day';
} else if (range <= 12 * 31 * 24 * 3600) {
// up to one year range loads weekly data
return 'week';
} else if (range <= 12 * 31 * 24 * 3600) {
// up to 5 year range loads monthly data
return 'month';
} else {
// greater range loads yearly data
return 'year';
}
}
private getTradesSummarized(trades: TradesData[], timestamp_from: number, interval?: string): SummarizedIntervals {
const intervals: any = {};
const intervals_prices: any = {};
for (const trade of trades) {
const traded_at = trade.tradeDate / 1000;
const interval_start = !interval ? timestamp_from : this.intervalStart(traded_at, interval);
if (!intervals[interval_start]) {
intervals[interval_start] = {
'open': 0,
'close': 0,
'high': 0,
'low': 0,
'avg': 0,
'volume_right': 0,
'volume_left': 0,
};
intervals_prices[interval_start] = [];
}
const period = intervals[interval_start];
const price = trade._tradePrice;
if (!intervals_prices[interval_start]['leftvol']) {
intervals_prices[interval_start]['leftvol'] = [];
}
if (!intervals_prices[interval_start]['rightvol']) {
intervals_prices[interval_start]['rightvol'] = [];
}
intervals_prices[interval_start]['leftvol'].push(trade._tradeAmount);
intervals_prices[interval_start]['rightvol'].push(trade._tradeVolume);
if (price) {
const plow = period['low'];
period['period_start'] = interval_start;
period['open'] = period['open'] || price;
period['close'] = price;
period['high'] = price > period['high'] ? price : period['high'];
period['low'] = (plow && price > plow) ? period['low'] : price;
period['avg'] = intervals_prices[interval_start]['rightvol'].reduce((p: number, c: number) => c + p, 0)
/ intervals_prices[interval_start]['leftvol'].reduce((c: number, p: number) => c + p, 0) * 100000000;
period['volume_left'] += trade._tradeAmount;
period['volume_right'] += trade._tradeVolume;
}
}
return intervals;
}
private getTradesByCriteria(
market: string | undefined,
timestamp_to: number,
timestamp_from: number,
trade_id_to: string | undefined,
trade_id_from: string | undefined,
direction: 'buy' | 'sell' | undefined,
sort: string,
limit: number,
integerAmounts: boolean = true,
): TradesData[] {
let trade_id_from_ts: number | null = null;
let trade_id_to_ts: number | null = null;
const allCurrencies = this.getCurrencies();
const timestampFromMilli = timestamp_from * 1000;
const timestampToMilli = timestamp_to * 1000;
// note: the offer_id_from/to depends on iterating over trades in
// descending chronological order.
const tradesDataSorted = this.tradesData.slice();
if (sort === 'asc') {
tradesDataSorted.reverse();
}
let matches: TradesData[] = [];
for (const trade of tradesDataSorted) {
if (trade_id_from === trade.offerId) {
trade_id_from_ts = trade.tradeDate;
}
if (trade_id_to === trade.offerId) {
trade_id_to_ts = trade.tradeDate;
}
if (trade_id_to && trade_id_to_ts === null) {
continue;
}
if (trade_id_from && trade_id_from_ts != null && trade_id_from_ts !== trade.tradeDate) {
continue;
}
if (market && market !== trade._market) {
continue;
}
if (timestampFromMilli && timestampFromMilli > trade.tradeDate) {
continue;
}
if (timestampToMilli && timestampToMilli < trade.tradeDate) {
continue;
}
if (direction && direction !== trade.direction.toLowerCase()) {
continue;
}
// Filter out bogus trades with BTC/BTC or XXX/XXX market.
// See github issue: https://github.com/bitsquare/bitsquare/issues/883
const currencyPairs = trade.currencyPair.split('/');
if (currencyPairs[0] === currencyPairs[1]) {
continue;
}
const currencyLeft = allCurrencies[currencyPairs[0]];
const currencyRight = allCurrencies[currencyPairs[1]];
if (!currencyLeft || !currencyRight) {
continue;
}
const tradePrice = trade.primaryMarketTradePrice * Math.pow(10, 8 - currencyRight.precision);
const tradeAmount = trade.primaryMarketTradeAmount * Math.pow(10, 8 - currencyLeft.precision);
const tradeVolume = trade.primaryMarketTradeVolume * Math.pow(10, 8 - currencyRight.precision);
if (integerAmounts) {
trade._tradePrice = tradePrice;
trade._tradeAmount = tradeAmount;
trade._tradeVolume = tradeVolume;
trade._offerAmount = trade.offerAmount;
} else {
trade._tradePriceStr = this.intToBtc(tradePrice);
trade._tradeAmountStr = this.intToBtc(tradeAmount);
trade._tradeVolumeStr = this.intToBtc(tradeVolume);
trade._offerAmountStr = this.intToBtc(trade.offerAmount);
}
matches.push(trade);
if (matches.length >= limit) {
break;
}
}
if ((trade_id_from && !trade_id_from_ts) || (trade_id_to && !trade_id_to_ts)) {
matches = [];
}
return matches;
}
private intervalStart(ts: number, interval: string): number {
switch (interval) {
case 'minute':
return (ts - (ts % 60));
case '10_minute':
return (ts - (ts % 600));
case 'half_hour':
return (ts - (ts % 1800));
case 'hour':
return (ts - (ts % 3600));
case 'half_day':
return (ts - (ts % (3600 * 12)));
case 'day':
return datetime.strtotime('midnight today', ts);
case 'week':
return datetime.strtotime('midnight sunday last week', ts);
case 'month':
return datetime.strtotime('midnight first day of this month', ts);
case 'year':
return datetime.strtotime('midnight first day of january', ts);
default:
throw new Error('Unsupported interval: ' + interval);
}
}
private offerDataToOffer(offer: OffersData, market: string): Offer {
const currencyPairs = market.split('_');
const currencyRight = this.allCurrenciesIndexed[currencyPairs[1].toUpperCase()];
const currencyLeft = this.allCurrenciesIndexed[currencyPairs[0].toUpperCase()];
const price = offer['primaryMarketPrice'] * Math.pow( 10, 8 - currencyRight['precision']);
const amount = offer['primaryMarketAmount'] * Math.pow( 10, 8 - currencyLeft['precision']);
const volume = offer['primaryMarketVolume'] * Math.pow( 10, 8 - currencyRight['precision']);
return {
offer_id: offer.id,
offer_date: offer.date,
direction: offer.primaryMarketDirection,
min_amount: this.intToBtc(offer.minAmount),
amount: this.intToBtc(amount),
price: this.intToBtc(price),
volume: this.intToBtc(volume),
payment_method: offer.paymentMethod,
offer_fee_txid: null,
};
}
private intToBtc(val: number): string {
return (val / 100000000).toFixed(8);
}
}
export default new BisqMarketsApi();

View File

@@ -0,0 +1,131 @@
import config from '../../config';
import * as fs from 'fs';
import { OffersData as OffersData, TradesData, Currency } from './interfaces';
import bisqMarket from './markets-api';
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_FILE_PATHS = {
activeCryptoCurrency: '/active_crypto_currency_list.json',
activeFiatCurrency: '/active_fiat_currency_list.json',
cryptoCurrency: '/crypto_currency_list.json',
fiatCurrency: '/fiat_currency_list.json',
offers: '/offers_statistics.json',
trades: '/trade_statistics.json',
};
private cryptoCurrencyLastMtime = new Date('2016-01-01');
private fiatCurrencyLastMtime = new Date('2016-01-01');
private offersLastMtime = new Date('2016-01-01');
private tradesLastMtime = new Date('2016-01-01');
private subdirectoryWatcher: fs.FSWatcher | undefined;
constructor() {}
startBisqService(): void {
this.checkForBisqDataFolder();
this.loadBisqDumpFile();
this.startBisqDirectoryWatcher();
}
private checkForBisqDataFolder() {
if (!fs.existsSync(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency)) {
logger.err(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency + ` doesn't exist. Make sure Bisq is running and the config is correct before starting the server.`);
return process.exit(1);
}
}
private startBisqDirectoryWatcher() {
if (this.subdirectoryWatcher) {
this.subdirectoryWatcher.close();
}
if (!fs.existsSync(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency)) {
logger.warn(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency + ` doesn't exist. Trying to restart sub directory watcher again in 3 minutes.`);
setTimeout(() => this.startBisqDirectoryWatcher(), 180000);
return;
}
let fsWait: NodeJS.Timeout | null = null;
this.subdirectoryWatcher = fs.watch(Bisq.MARKET_JSON_PATH, () => {
if (fsWait) {
clearTimeout(fsWait);
}
fsWait = setTimeout(() => {
logger.debug(`Change detected in the Bisq market data folder.`);
this.loadBisqDumpFile();
}, Bisq.FOLDER_WATCH_CHANGE_DETECTION_DEBOUNCE);
});
}
private async loadBisqDumpFile(): Promise<void> {
const start = new Date().getTime();
try {
let marketsDataUpdated = false;
const cryptoMtime = this.getFileMtime(Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency);
const fiatMtime = this.getFileMtime(Bisq.MARKET_JSON_FILE_PATHS.fiatCurrency);
if (cryptoMtime > this.cryptoCurrencyLastMtime || fiatMtime > this.fiatCurrencyLastMtime) {
const cryptoCurrencyData = await this.loadData<Currency[]>(Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency);
const fiatCurrencyData = await this.loadData<Currency[]>(Bisq.MARKET_JSON_FILE_PATHS.fiatCurrency);
const activeCryptoCurrencyData = await this.loadData<Currency[]>(Bisq.MARKET_JSON_FILE_PATHS.activeCryptoCurrency);
const activeFiatCurrencyData = await this.loadData<Currency[]>(Bisq.MARKET_JSON_FILE_PATHS.activeFiatCurrency);
logger.debug('Updating Bisq Market Currency Data');
bisqMarket.setCurrencyData(cryptoCurrencyData, fiatCurrencyData, activeCryptoCurrencyData, activeFiatCurrencyData);
if (cryptoMtime > this.cryptoCurrencyLastMtime) {
this.cryptoCurrencyLastMtime = cryptoMtime;
}
if (fiatMtime > this.fiatCurrencyLastMtime) {
this.fiatCurrencyLastMtime = fiatMtime;
}
marketsDataUpdated = true;
}
const offersMtime = this.getFileMtime(Bisq.MARKET_JSON_FILE_PATHS.offers);
if (offersMtime > this.offersLastMtime) {
const offersData = await this.loadData<OffersData[]>(Bisq.MARKET_JSON_FILE_PATHS.offers);
logger.debug('Updating Bisq Market Offers Data');
bisqMarket.setOffersData(offersData);
this.offersLastMtime = offersMtime;
marketsDataUpdated = true;
}
const tradesMtime = this.getFileMtime(Bisq.MARKET_JSON_FILE_PATHS.trades);
if (tradesMtime > this.tradesLastMtime) {
const tradesData = await this.loadData<TradesData[]>(Bisq.MARKET_JSON_FILE_PATHS.trades);
logger.debug('Updating Bisq Market Trades Data');
bisqMarket.setTradesData(tradesData);
this.tradesLastMtime = tradesMtime;
marketsDataUpdated = true;
}
if (marketsDataUpdated) {
bisqMarket.updateCache();
const time = new Date().getTime() - start;
logger.debug('Bisq market data updated in ' + time + ' ms');
}
} catch (e) {
logger.err('loadBisqMarketDataDumpFile() error.' + e.message || e);
}
}
private getFileMtime(path: string): Date {
const stats = fs.statSync(Bisq.MARKET_JSON_PATH + path);
return stats.mtime;
}
private loadData<T>(path: string): Promise<T> {
return new Promise((resolve, reject) => {
fs.readFile(Bisq.MARKET_JSON_PATH + path, 'utf8', (err, data) => {
if (err) {
reject(err);
}
try {
const parsedData = JSON.parse(data);
resolve(parsedData);
} catch (e) {
reject('JSON parse error (' + path + ')');
}
});
});
}
}
export default new Bisq();

View File

@@ -0,0 +1,14 @@
import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi {
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean): Promise<IEsploraApi.Transaction>;
$getRawTransactionBitcoind(txId: string, skipConversion?: boolean, addPrevout?: boolean): Promise<IEsploraApi.Transaction>;
$getBlockHeightTip(): Promise<number>;
$getTxIdsForBlock(hash: string): Promise<string[]>;
$getBlockHash(height: number): Promise<string>;
$getBlock(hash: string): Promise<IEsploraApi.Block>;
$getAddress(address: string): Promise<IEsploraApi.Address>;
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
$getAddressPrefix(prefix: string): string[];
}

View File

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

View File

@@ -0,0 +1,116 @@
export namespace IBitcoinApi {
export interface MempoolInfo {
loaded: boolean; // (boolean) True if the mempool is fully loaded
size: number; // (numeric) Current tx count
bytes: number; // (numeric) Sum of all virtual transaction sizes as defined in BIP 141.
usage: number; // (numeric) Total memory usage for the mempool
maxmempool: number; // (numeric) Maximum memory usage for the mempool
mempoolminfee: number; // (numeric) Minimum fee rate in BTC/kB for tx to be accepted.
minrelaytxfee: number; // (numeric) Current minimum relay fee for transactions
}
export interface RawMempool { [txId: string]: MempoolEntry; }
export interface MempoolEntry {
vsize: number; // (numeric) virtual transaction size as defined in BIP 141.
weight: number; // (numeric) transaction weight as defined in BIP 141.
time: number; // (numeric) local time transaction entered pool in seconds since 1 Jan 1970 GMT
height: number; // (numeric) block height when transaction entered pool
descendantcount: number; // (numeric) number of in-mempool descendant transactions (including this one)
descendantsize: number; // (numeric) virtual transaction size of in-mempool descendants (including this one)
ancestorcount: number; // (numeric) number of in-mempool ancestor transactions (including this one)
ancestorsize: number; // (numeric) virtual transaction size of in-mempool ancestors (including this one)
wtxid: string; // (string) hash of serialized transactionumber; including witness data
fees: {
base: number; // (numeric) transaction fee in BTC
modified: number; // (numeric) transaction fee with fee deltas used for mining priority in BTC
ancestor: number; // (numeric) modified fees (see above) of in-mempool ancestors (including this one) in BTC
descendant: number; // (numeric) modified fees (see above) of in-mempool descendants (including this one) in BTC
};
depends: string[]; // (string) parent transaction id
spentby: string[]; // (array) unconfirmed transactions spending outputs from this transaction
'bip125-replaceable': boolean; // (boolean) Whether this transaction could be replaced due to BIP125 (replace-by-fee)
}
export interface Block {
hash: string; // (string) the block hash (same as provided)
confirmations: number; // (numeric) The number of confirmations, or -1 if the block is not on the main chain
size: number; // (numeric) The block size
strippedsize: number; // (numeric) The block size excluding witness data
weight: number; // (numeric) The block weight as defined in BIP 141
height: number; // (numeric) The block height or index
version: number; // (numeric) The block version
versionHex: string; // (string) The block version formatted in hexadecimal
merkleroot: string; // (string) The merkle root
tx: Transaction[];
time: number; // (numeric) The block time expressed in UNIX epoch time
mediantime: number; // (numeric) The median block time expressed in UNIX epoch time
nonce: number; // (numeric) The nonce
bits: string; // (string) The bits
difficulty: number; // (numeric) The difficulty
chainwork: string; // (string) Expected number of hashes required to produce the chain up to this block (in hex)
nTx: number; // (numeric) The number of transactions in the block
previousblockhash: string; // (string) The hash of the previous block
nextblockhash: string; // (string) The hash of the next block
}
export interface Transaction {
in_active_chain: boolean; // (boolean) Whether specified block is in the active chain or not
hex: string; // (string) The serialized, hex-encoded data for 'txid'
txid: string; // (string) The transaction id (same as provided)
hash: string; // (string) The transaction hash (differs from txid for witness transactions)
size: number; // (numeric) The serialized transaction size
vsize: number; // (numeric) The virtual transaction size (differs from size for witness transactions)
weight: number; // (numeric) The transaction's weight (between vsize*4-3 and vsize*4)
version: number; // (numeric) The version
locktime: number; // (numeric) The lock time
vin: Vin[];
vout: Vout[];
blockhash: string; // (string) the block hash
confirmations: number; // (numeric) The confirmations
blocktime: number; // (numeric) The block time expressed in UNIX epoch time
time: number; // (numeric) Same as blocktime
}
interface Vin {
txid?: string; // (string) The transaction id
vout?: number; // (string)
scriptSig?: { // (json object) The script
asm: string; // (string) asm
hex: string; // (string) hex
};
sequence: number; // (numeric) The script sequence number
txinwitness?: string[]; // (string) hex-encoded witness data
coinbase?: string;
}
interface Vout {
value: number; // (numeric) The value in BTC
n: number; // (numeric) index
scriptPubKey: { // (json object)
asm: string; // (string) the asm
hex: string; // (string) the hex
reqSigs: number; // (numeric) The required sigs
type: string; // (string) The type, eg 'pubkeyhash'
addresses: string[] // (string) bitcoin address
};
}
export interface AddressInformation {
isvalid: boolean; // (boolean) If the address is valid or not. If not, this is the only property returned.
address: string; // (string) The bitcoin address validated
scriptPubKey: string; // (string) The hex-encoded scriptPubKey generated by the address
isscript: boolean; // (boolean) If the key is a script
iswitness: boolean; // (boolean) If the address is a witness
witness_version?: boolean; // (numeric, optional) The version number of the witness program
witness_program: string; // (string, optional) The hex value of the witness program
}
export interface ChainTips {
height: number; // (numeric) height of the chain tip
hash: string; // (string) block hash of the tip
branchlen: number; // (numeric) zero for main chain, otherwise length of branch connecting the tip to the main chain
status: 'invalid' | 'headers-only' | 'valid-headers' | 'valid-fork' | 'active';
}
}

View File

@@ -0,0 +1,309 @@
import config from '../../config';
import * as bitcoin from '@mempool/bitcoin';
import * as bitcoinjs from 'bitcoinjs-lib';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { IBitcoinApi } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface';
import blocks from '../blocks';
import mempool from '../mempool';
import { TransactionExtended } from '../../mempool.interfaces';
class BitcoinApi implements AbstractBitcoinApi {
private rawMempoolCache: IBitcoinApi.RawMempool | null = null;
private bitcoindClient: any;
constructor() {
this.bitcoindClient = new bitcoin.Client({
host: config.CORE_RPC.HOST,
port: config.CORE_RPC.PORT,
user: config.CORE_RPC.USERNAME,
pass: config.CORE_RPC.PASSWORD,
timeout: 60000,
});
}
$getRawTransactionBitcoind(txId: string, skipConversion = false, addPrevout = false): Promise<IEsploraApi.Transaction> {
return this.bitcoindClient.getRawTransaction(txId, true)
.then((transaction: IBitcoinApi.Transaction) => {
if (skipConversion) {
return transaction;
}
return this.$convertTransaction(transaction, addPrevout);
});
}
$getRawTransaction(txId: string, skipConversion = false, addPrevout = false): Promise<IEsploraApi.Transaction> {
// If the transaction is in the mempool we already converted and fetched the fee. Only prevouts are missing
const txInMempool = mempool.getMempool()[txId];
if (txInMempool && addPrevout) {
return this.$addPrevouts(txInMempool);
}
// Special case to fetch the Coinbase transaction
if (txId === '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b') {
return this.$returnCoinbaseTransaction();
}
return this.bitcoindClient.getRawTransaction(txId, true)
.then((transaction: IBitcoinApi.Transaction) => {
if (skipConversion) {
return transaction;
}
return this.$convertTransaction(transaction, addPrevout);
});
}
$getBlockHeightTip(): Promise<number> {
return this.bitcoindClient.getChainTips()
.then((result: IBitcoinApi.ChainTips[]) => result[0].height);
}
$getTxIdsForBlock(hash: string): Promise<string[]> {
return this.bitcoindClient.getBlock(hash, 1)
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
}
$getBlockHash(height: number): Promise<string> {
return this.bitcoindClient.getBlockHash(height);
}
async $getBlock(hash: string): Promise<IEsploraApi.Block> {
const foundBlock = blocks.getBlocks().find((block) => block.id === hash);
if (foundBlock) {
return foundBlock;
}
return this.bitcoindClient.getBlock(hash)
.then((block: IBitcoinApi.Block) => this.convertBlock(block));
}
$getAddress(address: string): Promise<IEsploraApi.Address> {
throw new Error('Method getAddress not supported by the Bitcoin RPC API.');
}
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getAddressTransactions not supported by the Bitcoin RPC API.');
}
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
return this.bitcoindClient.getRawMemPool();
}
$getAddressPrefix(prefix: string): string[] {
const found: string[] = [];
const mp = mempool.getMempool();
for (const tx in mp) {
for (const vout of mp[tx].vout) {
if (vout.scriptpubkey_address.indexOf(prefix) === 0) {
found.push(vout.scriptpubkey_address);
if (found.length >= 10) {
return found;
}
}
}
}
return found;
}
protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean): Promise<IEsploraApi.Transaction> {
let esploraTransaction: IEsploraApi.Transaction = {
txid: transaction.txid,
version: transaction.version,
locktime: transaction.locktime,
size: transaction.size,
weight: transaction.weight,
fee: 0,
vin: [],
vout: [],
status: { confirmed: false },
};
esploraTransaction.vout = transaction.vout.map((vout) => {
return {
value: vout.value * 100000000,
scriptpubkey: vout.scriptPubKey.hex,
scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '',
scriptpubkey_asm: vout.scriptPubKey.asm ? this.convertScriptSigAsm(vout.scriptPubKey.asm) : '',
scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type),
};
});
esploraTransaction.vin = transaction.vin.map((vin) => {
return {
is_coinbase: !!vin.coinbase,
prevout: null,
scriptsig: vin.scriptSig && vin.scriptSig.hex || vin.coinbase || '',
scriptsig_asm: vin.scriptSig && this.convertScriptSigAsm(vin.scriptSig.asm) || '',
sequence: vin.sequence,
txid: vin.txid || '',
vout: vin.vout || 0,
witness: vin.txinwitness,
};
});
if (transaction.confirmations) {
esploraTransaction.status = {
confirmed: true,
block_height: blocks.getCurrentBlockHeight() - transaction.confirmations + 1,
block_hash: transaction.blockhash,
block_time: transaction.blocktime,
};
}
if (transaction.confirmations) {
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, addPrevout);
} else {
esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction);
}
return esploraTransaction;
}
private convertBlock(block: IBitcoinApi.Block): IEsploraApi.Block {
return {
id: block.hash,
height: block.height,
version: block.version,
timestamp: block.time,
bits: parseInt(block.bits, 16),
nonce: block.nonce,
difficulty: block.difficulty,
merkle_root: block.merkleroot,
tx_count: block.nTx,
size: block.size,
weight: block.weight,
previousblockhash: block.previousblockhash,
};
}
private translateScriptPubKeyType(outputType: string): string {
const map = {
'pubkey': 'p2pk',
'pubkeyhash': 'p2pkh',
'scripthash': 'p2sh',
'witness_v0_keyhash': 'v0_p2wpkh',
'witness_v0_scripthash': 'v0_p2wsh',
'witness_v1_taproot': 'v1_p2tr',
'nonstandard': 'nonstandard',
'nulldata': 'op_return'
};
if (map[outputType]) {
return map[outputType];
} else {
return '';
}
}
private async $appendMempoolFeeData(transaction: IEsploraApi.Transaction): Promise<IEsploraApi.Transaction> {
if (transaction.fee) {
return transaction;
}
let mempoolEntry: IBitcoinApi.MempoolEntry;
if (!mempool.isInSync() && !this.rawMempoolCache) {
this.rawMempoolCache = await this.$getRawMempoolVerbose();
}
if (this.rawMempoolCache && this.rawMempoolCache[transaction.txid]) {
mempoolEntry = this.rawMempoolCache[transaction.txid];
} else {
mempoolEntry = await this.$getMempoolEntry(transaction.txid);
}
transaction.fee = mempoolEntry.fees.base * 100000000;
return transaction;
}
protected async $addPrevouts(transaction: TransactionExtended): Promise<TransactionExtended> {
for (const vin of transaction.vin) {
if (vin.prevout) {
continue;
}
const innerTx = await this.$getRawTransaction(vin.txid, false);
vin.prevout = innerTx.vout[vin.vout];
this.addInnerScriptsToVin(vin);
}
return transaction;
}
protected $returnCoinbaseTransaction(): Promise<IEsploraApi.Transaction> {
return this.bitcoindClient.getBlock('000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f', 2)
.then((block: IBitcoinApi.Block) => {
return this.$convertTransaction(Object.assign(block.tx[0], {
confirmations: blocks.getCurrentBlockHeight() + 1,
blocktime: 1231006505 }), false);
});
}
protected $validateAddress(address: string): Promise<IBitcoinApi.AddressInformation> {
return this.bitcoindClient.validateAddress(address);
}
private $getMempoolEntry(txid: string): Promise<IBitcoinApi.MempoolEntry> {
return this.bitcoindClient.getMempoolEntry(txid);
}
private $getRawMempoolVerbose(): Promise<IBitcoinApi.RawMempool> {
return this.bitcoindClient.getRawMemPool(true);
}
private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction, addPrevout: boolean): Promise<IEsploraApi.Transaction> {
if (transaction.vin[0].is_coinbase) {
transaction.fee = 0;
return transaction;
}
let totalIn = 0;
for (const vin of transaction.vin) {
const innerTx = await this.$getRawTransaction(vin.txid, !addPrevout);
if (addPrevout) {
vin.prevout = innerTx.vout[vin.vout];
this.addInnerScriptsToVin(vin);
}
totalIn += innerTx.vout[vin.vout].value;
}
const totalOut = transaction.vout.reduce((p, output) => p + output.value, 0);
transaction.fee = parseFloat((totalIn - totalOut).toFixed(8));
return transaction;
}
private convertScriptSigAsm(str: string): string {
const a = str.split(' ');
const b: string[] = [];
a.forEach((chunk) => {
if (chunk.substr(0, 3) === 'OP_') {
chunk = chunk.replace(/^OP_(\d+)/, 'OP_PUSHNUM_$1');
chunk = chunk.replace('OP_CHECKSEQUENCEVERIFY', 'OP_CSV');
b.push(chunk);
} else {
chunk = chunk.replace('[ALL]', '01');
if (chunk === '0') {
b.push('OP_0');
} else {
b.push('OP_PUSHBYTES_' + Math.round(chunk.length / 2) + ' ' + chunk);
}
}
});
return b.join(' ');
}
private addInnerScriptsToVin(vin: IEsploraApi.Vin): void {
if (!vin.prevout) {
return;
}
if (vin.prevout.scriptpubkey_type === 'p2sh') {
const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0];
vin.inner_redeemscript_asm = this.convertScriptSigAsm(bitcoinjs.script.toASM(Buffer.from(redeemScript, 'hex')));
if (vin.witness && vin.witness.length > 2) {
const witnessScript = vin.witness[vin.witness.length - 1];
vin.inner_witnessscript_asm = this.convertScriptSigAsm(bitcoinjs.script.toASM(Buffer.from(witnessScript, 'hex')));
}
}
if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) {
const witnessScript = vin.witness[vin.witness.length - 1];
vin.inner_witnessscript_asm = this.convertScriptSigAsm(bitcoinjs.script.toASM(Buffer.from(witnessScript, 'hex')));
}
}
}
export default BitcoinApi;

View File

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

View File

@@ -1,146 +0,0 @@
const config = require('../../../mempool-config.json');
import { Transaction, Block, MempoolInfo } from '../../interfaces';
import * as request from 'request';
class ElectrsApi {
constructor() {
}
getMempoolInfo(): Promise<MempoolInfo> {
return new Promise((resolve, reject) => {
request(config.ELECTRS_API_URL + '/mempool', { json: true, timeout: 10000 }, (err, res, response) => {
if (err) {
reject(err);
} else if (res.statusCode !== 200) {
reject(response);
} else {
if (typeof response.count !== 'number') {
reject('Empty data');
return;
}
resolve({
size: response.count,
bytes: response.vsize,
});
}
});
});
}
getRawMempool(): Promise<Transaction['txid'][]> {
return new Promise((resolve, reject) => {
request(config.ELECTRS_API_URL + '/mempool/txids', { json: true, timeout: 10000, forever: true }, (err, res, response) => {
if (err) {
reject(err);
} else if (res.statusCode !== 200) {
reject(response);
} else {
if (response.constructor === Array) {
resolve(response);
} else {
reject('returned invalid data');
}
}
});
});
}
getRawTransaction(txId: string): Promise<Transaction> {
return new Promise((resolve, reject) => {
request(config.ELECTRS_API_URL + '/tx/' + txId, { json: true, timeout: 10000, forever: true }, (err, res, response) => {
if (err) {
reject(err);
} else if (res.statusCode !== 200) {
reject(response);
} else {
if (response.constructor === Object) {
resolve(response);
} else {
reject('returned invalid data');
}
}
});
});
}
getBlockHeightTip(): Promise<number> {
return new Promise((resolve, reject) => {
request(config.ELECTRS_API_URL + '/blocks/tip/height', { json: true, timeout: 10000 }, (err, res, response) => {
if (err) {
reject(err);
} else if (res.statusCode !== 200) {
reject(response);
} else {
resolve(response);
}
});
});
}
getTxIdsForBlock(hash: string): Promise<string[]> {
return new Promise((resolve, reject) => {
request(config.ELECTRS_API_URL + '/block/' + hash + '/txids', { json: true, timeout: 10000 }, (err, res, response) => {
if (err) {
reject(err);
} else if (res.statusCode !== 200) {
reject(response);
} else {
if (response.constructor === Array) {
resolve(response);
} else {
reject('returned invalid data');
}
}
});
});
}
getBlockHash(height: number): Promise<string> {
return new Promise((resolve, reject) => {
request(config.ELECTRS_API_URL + '/block-height/' + height, { json: true, timeout: 10000 }, (err, res, response) => {
if (err) {
reject(err);
} else if (res.statusCode !== 200) {
reject(response);
} else {
resolve(response);
}
});
});
}
getBlocksFromHeight(height: number): Promise<string> {
return new Promise((resolve, reject) => {
request(config.ELECTRS_API_URL + '/blocks/' + height, { json: true, timeout: 10000 }, (err, res, response) => {
if (err) {
reject(err);
} else if (res.statusCode !== 200) {
reject(response);
} else {
resolve(response);
}
});
});
}
getBlock(hash: string): Promise<Block> {
return new Promise((resolve, reject) => {
request(config.ELECTRS_API_URL + '/block/' + hash, { json: true, timeout: 10000 }, (err, res, response) => {
if (err) {
reject(err);
} else if (res.statusCode !== 200) {
reject(response);
} else {
if (response.constructor === Object) {
resolve(response);
} else {
reject('getBlock returned invalid data');
}
}
});
});
}
}
export default new ElectrsApi();

View File

@@ -0,0 +1,12 @@
export namespace IElectrumApi {
export interface ScriptHashBalance {
confirmed: number;
unconfirmed: number;
}
export interface ScriptHashHistory {
height: number;
tx_hash: string;
fee?: number;
}
}

View File

@@ -0,0 +1,161 @@
import config from '../../config';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { IBitcoinApi } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface';
import { IElectrumApi } from './electrum-api.interface';
import BitcoinApi from './bitcoin-api';
import mempool from '../mempool';
import logger from '../../logger';
import * as ElectrumClient from '@mempool/electrum-client';
import * as sha256 from 'crypto-js/sha256';
import * as hexEnc from 'crypto-js/enc-hex';
import loadingIndicators from '../loading-indicators';
import memoryCache from '../memory-cache';
class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
private electrumClient: any;
constructor() {
super();
const electrumConfig = { client: 'mempool-v2', version: '1.4' };
const electrumPersistencePolicy = { retryPeriod: 10000, maxRetry: 1000, callback: null };
const electrumCallbacks = {
onConnect: (client, versionInfo) => { logger.info(`Connected to Electrum Server at ${config.ELECTRUM.HOST}:${config.ELECTRUM.PORT} (${JSON.stringify(versionInfo)})`); },
onClose: (client) => { logger.info(`Disconnected from Electrum Server at ${config.ELECTRUM.HOST}:${config.ELECTRUM.PORT}`); },
onError: (err) => { logger.err(`Electrum error: ${JSON.stringify(err)}`); },
onLog: (str) => { logger.debug(str); },
};
this.electrumClient = new ElectrumClient(
config.ELECTRUM.PORT,
config.ELECTRUM.HOST,
config.ELECTRUM.TLS_ENABLED ? 'tls' : 'tcp',
null,
electrumCallbacks
);
this.electrumClient.initElectrum(electrumConfig, electrumPersistencePolicy)
.then(() => {})
.catch((err) => {
logger.err(`Error connecting to Electrum Server at ${config.ELECTRUM.HOST}:${config.ELECTRUM.PORT}`);
});
}
async $getAddress(address: string): Promise<IEsploraApi.Address> {
const addressInfo = await this.$validateAddress(address);
if (!addressInfo || !addressInfo.isvalid) {
return ({
'address': address,
'chain_stats': {
'funded_txo_count': 0,
'funded_txo_sum': 0,
'spent_txo_count': 0,
'spent_txo_sum': 0,
'tx_count': 0
},
'mempool_stats': {
'funded_txo_count': 0,
'funded_txo_sum': 0,
'spent_txo_count': 0,
'spent_txo_sum': 0,
'tx_count': 0
}
});
}
try {
const balance = await this.$getScriptHashBalance(addressInfo.scriptPubKey);
const history = await this.$getScriptHashHistory(addressInfo.scriptPubKey);
const unconfirmed = history.filter((h) => h.fee).length;
return {
'address': addressInfo.address,
'chain_stats': {
'funded_txo_count': 0,
'funded_txo_sum': balance.confirmed ? balance.confirmed : 0,
'spent_txo_count': 0,
'spent_txo_sum': balance.confirmed < 0 ? balance.confirmed : 0,
'tx_count': history.length - unconfirmed,
},
'mempool_stats': {
'funded_txo_count': 0,
'funded_txo_sum': balance.unconfirmed > 0 ? balance.unconfirmed : 0,
'spent_txo_count': 0,
'spent_txo_sum': balance.unconfirmed < 0 ? -balance.unconfirmed : 0,
'tx_count': unconfirmed,
},
'electrum': true,
};
} catch (e) {
if (e === 'failed to get confirmed status') {
e = 'The number of transactions on this address exceeds the Electrum server limit';
}
throw new Error(e);
}
}
async $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
const addressInfo = await this.$validateAddress(address);
if (!addressInfo || !addressInfo.isvalid) {
return [];
}
try {
loadingIndicators.setProgress('address-' + address, 0);
const transactions: IEsploraApi.Transaction[] = [];
const history = await this.$getScriptHashHistory(addressInfo.scriptPubKey);
history.sort((a, b) => (b.height || 9999999) - (a.height || 9999999));
let startingIndex = 0;
if (lastSeenTxId) {
const pos = history.findIndex((historicalTx) => historicalTx.tx_hash === lastSeenTxId);
if (pos) {
startingIndex = pos + 1;
}
}
const endIndex = Math.min(startingIndex + 10, history.length);
for (let i = startingIndex; i < endIndex; i++) {
const tx = await this.$getRawTransaction(history[i].tx_hash, false, true);
transactions.push(tx);
loadingIndicators.setProgress('address-' + address, (i + 1) / endIndex * 100);
}
return transactions;
} catch (e) {
loadingIndicators.setProgress('address-' + address, 100);
if (e === 'failed to get confirmed status') {
e = 'The number of transactions on this address exceeds the Electrum server limit';
}
throw new Error(e);
}
}
private $getScriptHashBalance(scriptHash: string): Promise<IElectrumApi.ScriptHashBalance> {
return this.electrumClient.blockchainScripthash_getBalance(this.encodeScriptHash(scriptHash));
}
private $getScriptHashHistory(scriptHash: string): Promise<IElectrumApi.ScriptHashHistory[]> {
const fromCache = memoryCache.get<IElectrumApi.ScriptHashHistory[]>('Scripthash_getHistory', scriptHash);
if (fromCache) {
return Promise.resolve(fromCache);
}
return this.electrumClient.blockchainScripthash_getHistory(this.encodeScriptHash(scriptHash))
.then((history) => {
memoryCache.set('Scripthash_getHistory', scriptHash, history, 2);
return history;
});
}
private encodeScriptHash(scriptPubKey: string): string {
const addrScripthash = hexEnc.stringify(sha256(hexEnc.parse(scriptPubKey)));
return addrScripthash.match(/.{2}/g).reverse().join('');
}
}
export default BitcoindElectrsApi;

View File

@@ -0,0 +1,169 @@
export namespace IEsploraApi {
export interface Transaction {
txid: string;
version: number;
locktime: number;
size: number;
weight: number;
fee: number;
vin: Vin[];
vout: Vout[];
status: Status;
}
export interface Recent {
txid: string;
fee: number;
vsize: number;
value: number;
}
export interface Vin {
txid: string;
vout: number;
is_coinbase: boolean;
scriptsig: string;
scriptsig_asm: string;
inner_redeemscript_asm?: string;
inner_witnessscript_asm?: string;
sequence: any;
witness?: string[];
prevout: Vout | null;
// Elements
is_pegin?: boolean;
issuance?: Issuance;
}
interface Issuance {
asset_id: string;
is_reissuance: string;
asset_blinding_nonce: string;
asset_entropy: string;
contract_hash: string;
assetamount?: number;
assetamountcommitment?: string;
tokenamount?: number;
tokenamountcommitment?: string;
}
export interface Vout {
scriptpubkey: string;
scriptpubkey_asm: string;
scriptpubkey_type: string;
scriptpubkey_address: string;
value: number;
// Elements
valuecommitment?: number;
asset?: string;
pegout?: Pegout;
}
interface Pegout {
genesis_hash: string;
scriptpubkey: string;
scriptpubkey_asm: string;
scriptpubkey_address: string;
}
export interface Status {
confirmed: boolean;
block_height?: number;
block_hash?: string;
block_time?: number;
}
export interface Block {
id: string;
height: number;
version: number;
timestamp: number;
bits: number;
nonce: number;
difficulty: number;
merkle_root: string;
tx_count: number;
size: number;
weight: number;
previousblockhash: string;
}
export interface Address {
address: string;
chain_stats: ChainStats;
mempool_stats: MempoolStats;
electrum?: boolean;
}
export interface ChainStats {
funded_txo_count: number;
funded_txo_sum: number;
spent_txo_count: number;
spent_txo_sum: number;
tx_count: number;
}
export interface MempoolStats {
funded_txo_count: number;
funded_txo_sum: number;
spent_txo_count: number;
spent_txo_sum: number;
tx_count: number;
}
export interface Outspend {
spent: boolean;
txid: string;
vin: number;
status: Status;
}
export interface Asset {
asset_id: string;
issuance_txin: IssuanceTxin;
issuance_prevout: IssuancePrevout;
reissuance_token: string;
contract_hash: string;
status: Status;
chain_stats: AssetStats;
mempool_stats: AssetStats;
}
export interface AssetExtended extends Asset {
name: string;
ticker: string;
precision: number;
entity: Entity;
version: number;
issuer_pubkey: string;
}
export interface Entity {
domain: string;
}
interface IssuanceTxin {
txid: string;
vin: number;
}
interface IssuancePrevout {
txid: string;
vout: number;
}
interface AssetStats {
tx_count: number;
issuance_count: number;
issued_amount: number;
burned_amount: number;
has_blinded_issuances: boolean;
reissuance_tokens: number;
burned_reissuance_tokens: number;
peg_in_count: number;
peg_in_amount: number;
peg_out_count: number;
peg_out_amount: number;
burn_count: number;
}
}

View File

@@ -0,0 +1,61 @@
import config from '../../config';
import axios, { AxiosRequestConfig } from 'axios';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface';
class ElectrsApi implements AbstractBitcoinApi {
axiosConfig: AxiosRequestConfig = {
timeout: 10000,
};
constructor() { }
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
return axios.get<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids', this.axiosConfig)
.then((response) => response.data);
}
$getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
return axios.get<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId, this.axiosConfig)
.then((response) => response.data);
}
$getBlockHeightTip(): Promise<number> {
return axios.get<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig)
.then((response) => response.data);
}
$getTxIdsForBlock(hash: string): Promise<string[]> {
return axios.get<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig)
.then((response) => response.data);
}
$getBlockHash(height: number): Promise<string> {
return axios.get<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height, this.axiosConfig)
.then((response) => response.data);
}
$getBlock(hash: string): Promise<IEsploraApi.Block> {
return axios.get<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash, this.axiosConfig)
.then((response) => response.data);
}
$getAddress(address: string): Promise<IEsploraApi.Address> {
throw new Error('Method getAddress not implemented.');
}
$getAddressTransactions(address: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getAddressTransactions not implemented.');
}
$getRawTransactionBitcoind(txId: string): Promise<IEsploraApi.Transaction> {
return axios.get<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId, this.axiosConfig)
.then((response) => response.data);
}
$getAddressPrefix(prefix: string): string[] {
throw new Error('Method not implemented.');
}
}
export default ElectrsApi;

View File

@@ -1,107 +1,122 @@
const config = require('../../mempool-config.json');
import bitcoinApi from './bitcoin/electrs-api';
import config from '../config';
import bitcoinApi from './bitcoin/bitcoin-api-factory';
import logger from '../logger';
import memPool from './mempool';
import { Block, TransactionExtended, TransactionMinerInfo } from '../interfaces';
import { BlockExtended, TransactionExtended } from '../mempool.interfaces';
import { Common } from './common';
import diskCache from './disk-cache';
import transactionUtils from './transaction-utils';
class Blocks {
private blocks: Block[] = [];
private static INITIAL_BLOCK_AMOUNT = 8;
private blocks: BlockExtended[] = [];
private currentBlockHeight = 0;
private newBlockCallback: ((block: Block, txIds: string[], transactions: TransactionExtended[]) => void) | undefined;
private lastDifficultyAdjustmentTime = 0;
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
constructor() { }
public getBlocks(): Block[] {
public getBlocks(): BlockExtended[] {
return this.blocks;
}
public setBlocks(blocks: Block[]) {
public setBlocks(blocks: BlockExtended[]) {
this.blocks = blocks;
}
public setNewBlockCallback(fn: (block: Block, txIds: string[], transactions: TransactionExtended[]) => void) {
this.newBlockCallback = fn;
public setNewBlockCallback(fn: (block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void) {
this.newBlockCallbacks.push(fn);
}
public async updateBlocks() {
try {
const blockHeightTip = await bitcoinApi.getBlockHeightTip();
public async $updateBlocks() {
const blockHeightTip = await bitcoinApi.$getBlockHeightTip();
if (this.blocks.length === 0) {
this.currentBlockHeight = blockHeightTip - config.INITIAL_BLOCK_AMOUNT;
if (this.blocks.length === 0) {
this.currentBlockHeight = blockHeightTip - Blocks.INITIAL_BLOCK_AMOUNT;
} else {
this.currentBlockHeight = this.blocks[this.blocks.length - 1].height;
}
if (blockHeightTip - this.currentBlockHeight > Blocks.INITIAL_BLOCK_AMOUNT * 2) {
logger.info(`${blockHeightTip - this.currentBlockHeight} blocks since tip. Fast forwarding to the ${Blocks.INITIAL_BLOCK_AMOUNT} recent blocks`);
this.currentBlockHeight = blockHeightTip - Blocks.INITIAL_BLOCK_AMOUNT;
}
if (!this.lastDifficultyAdjustmentTime) {
const heightDiff = blockHeightTip % 2016;
const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff);
const block = await bitcoinApi.$getBlock(blockHash);
this.lastDifficultyAdjustmentTime = block.timestamp;
}
while (this.currentBlockHeight < blockHeightTip) {
if (this.currentBlockHeight === 0) {
this.currentBlockHeight = blockHeightTip;
} else {
this.currentBlockHeight = this.blocks[this.blocks.length - 1].height;
this.currentBlockHeight++;
logger.debug(`New block found (#${this.currentBlockHeight})!`);
}
if (blockHeightTip - this.currentBlockHeight > config.INITIAL_BLOCK_AMOUNT * 2) {
console.log(`${blockHeightTip - this.currentBlockHeight} blocks since tip. Fast forwarding to the ${config.INITIAL_BLOCK_AMOUNT} recent blocks`);
this.currentBlockHeight = blockHeightTip - config.INITIAL_BLOCK_AMOUNT;
}
const transactions: TransactionExtended[] = [];
while (this.currentBlockHeight < blockHeightTip) {
if (this.currentBlockHeight === 0) {
this.currentBlockHeight = blockHeightTip;
} else {
this.currentBlockHeight++;
console.log(`New block found (#${this.currentBlockHeight})!`);
}
const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight);
const block = await bitcoinApi.$getBlock(blockHash);
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
const blockHash = await bitcoinApi.getBlockHash(this.currentBlockHeight);
const block = await bitcoinApi.getBlock(blockHash);
const txIds = await bitcoinApi.getTxIdsForBlock(blockHash);
const mempool = memPool.getMempool();
let transactionsFound = 0;
const mempool = memPool.getMempool();
let found = 0;
let notFound = 0;
const transactions: TransactionExtended[] = [];
for (let i = 0; i < txIds.length; i++) {
if (mempool[txIds[i]]) {
transactions.push(mempool[txIds[i]]);
found++;
} else {
console.log(`Fetching block tx ${i} of ${txIds.length}`);
const tx = await memPool.getTransactionExtended(txIds[i]);
if (tx) {
transactions.push(tx);
for (let i = 0; i < txIds.length; i++) {
if (mempool[txIds[i]]) {
transactions.push(mempool[txIds[i]]);
transactionsFound++;
} else if (config.MEMPOOL.BACKEND === 'esplora' || memPool.isInSync() || i === 0) {
logger.debug(`Fetching block tx ${i} of ${txIds.length}`);
try {
const tx = await transactionUtils.$getTransactionExtended(txIds[i]);
transactions.push(tx);
} catch (e) {
logger.debug('Error fetching block tx: ' + e.message || e);
if (i === 0) {
throw new Error('Failed to fetch Coinbase transaction: ' + txIds[i]);
}
notFound++;
}
}
console.log(`${found} of ${txIds.length} found in mempool. ${notFound} not found.`);
block.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0);
block.coinbaseTx = this.stripCoinbaseTransaction(transactions[0]);
transactions.sort((a, b) => b.feePerVsize - a.feePerVsize);
block.medianFee = transactions.length > 1 ? Common.median(transactions.map((tx) => tx.feePerVsize)) : 0;
block.feeRange = transactions.length > 1 ? Common.getFeesInRange(transactions.slice(0, transactions.length - 1), 8) : [0, 0];
this.blocks.push(block);
if (this.blocks.length > config.KEEP_BLOCK_AMOUNT) {
this.blocks.shift();
}
if (this.newBlockCallback) {
this.newBlockCallback(block, txIds, transactions);
}
}
} catch (err) {
console.log('updateBlocks error', err);
logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${txIds.length - transactionsFound} not found.`);
const blockExtended: BlockExtended = Object.assign({}, block);
blockExtended.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0);
blockExtended.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]);
transactions.sort((a, b) => b.feePerVsize - a.feePerVsize);
blockExtended.medianFee = transactions.length > 1 ? Common.median(transactions.map((tx) => tx.feePerVsize)) : 0;
blockExtended.feeRange = transactions.length > 1 ? Common.getFeesInRange(transactions.slice(0, transactions.length - 1), 8) : [0, 0];
if (block.height % 2016 === 0) {
this.lastDifficultyAdjustmentTime = block.timestamp;
}
this.blocks.push(blockExtended);
if (this.blocks.length > Blocks.INITIAL_BLOCK_AMOUNT * 4) {
this.blocks = this.blocks.slice(-Blocks.INITIAL_BLOCK_AMOUNT * 4);
}
if (this.newBlockCallbacks.length) {
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
}
if (memPool.isInSync()) {
diskCache.$saveCacheToDisk();
}
}
}
private stripCoinbaseTransaction(tx: TransactionExtended): TransactionMinerInfo {
return {
vin: [{
scriptsig: tx.vin[0].scriptsig
}],
vout: tx.vout
.map((vout) => ({ scriptpubkey_address: vout.scriptpubkey_address, value: vout.value }))
.filter((vout) => vout.value)
};
public getLastDifficultyAdjustmentTime(): number {
return this.lastDifficultyAdjustmentTime;
}
public getCurrentBlockHeight(): number {
return this.currentBlockHeight;
}
}

View File

@@ -1,4 +1,4 @@
import { TransactionExtended } from '../interfaces';
import { TransactionExtended, TransactionStripped } from '../mempool.interfaces';
export class Common {
static median(numbers: number[]) {
@@ -47,4 +47,21 @@ export class Common {
});
return matches;
}
static stripTransaction(tx: TransactionExtended): TransactionStripped {
return {
txid: tx.txid,
fee: tx.fee,
vsize: tx.weight / 4,
value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
};
}
static sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}
}

View File

@@ -1,50 +1,87 @@
import * as fs from 'fs';
const fsPromises = fs.promises;
import * as cluster from 'cluster';
import memPool from './mempool';
import blocks from './blocks';
import logger from '../logger';
import config from '../config';
import { TransactionExtended } from '../mempool.interfaces';
class DiskCache {
static FILE_NAME = './cache.json';
private static FILE_NAME = config.MEMPOOL.CACHE_DIR + '/cache.json';
private static FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/cache{number}.json';
private static CHUNK_FILES = 25;
constructor() {
process.on('SIGINT', () => {
this.saveCacheToDisk();
process.exit(2);
});
constructor() { }
process.on('SIGTERM', () => {
this.saveCacheToDisk();
process.exit(2);
});
}
async $saveCacheToDisk(): Promise<void> {
if (!cluster.isMaster) {
return;
}
try {
logger.debug('Writing mempool and blocks data to disk cache (async)...');
saveCacheToDisk() {
this.saveData(JSON.stringify({
mempool: memPool.getMempool(),
blocks: blocks.getBlocks(),
}));
console.log('Mempool and blocks data saved to disk cache');
}
loadMempoolCache() {
const cacheData = this.loadData();
if (cacheData) {
console.log('Restoring mempool and blocks data from disk cache');
const data = JSON.parse(cacheData);
if (data.mempool) {
memPool.setMempool(data.mempool);
blocks.setBlocks(data.blocks);
} else {
memPool.setMempool(data);
const mempool = memPool.getMempool();
const mempoolArray: TransactionExtended[] = [];
for (const tx in mempool) {
mempoolArray.push(mempool[tx]);
}
const chunkSize = Math.floor(mempoolArray.length / DiskCache.CHUNK_FILES);
await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({
blocks: blocks.getBlocks(),
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), {flag: 'w'});
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
await fsPromises.writeFile(DiskCache.FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), {flag: 'w'});
}
logger.debug('Mempool and blocks data saved to disk cache');
} catch (e) {
logger.warn('Error writing to cache file: ' + e.message || e);
}
}
private saveData(dataBlob: string) {
fs.writeFileSync(DiskCache.FILE_NAME, dataBlob, 'utf8');
}
loadMempoolCache() {
if (!fs.existsSync(DiskCache.FILE_NAME)) {
return;
}
try {
let data: any = {};
const cacheData = fs.readFileSync(DiskCache.FILE_NAME, 'utf8');
if (cacheData) {
logger.info('Restoring mempool and blocks data from disk cache');
data = JSON.parse(cacheData);
if (data.mempoolArray) {
for (const tx of data.mempoolArray) {
data.mempool[tx.txid] = tx;
}
}
}
private loadData(): string {
return fs.readFileSync(DiskCache.FILE_NAME, 'utf8');
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
const fileName = DiskCache.FILE_NAMES.replace('{number}', i.toString());
if (fs.existsSync(fileName)) {
const cacheData2 = JSON.parse(fs.readFileSync(fileName, 'utf8'));
if (cacheData2.mempoolArray) {
for (const tx of cacheData2.mempoolArray) {
data.mempool[tx.txid] = tx;
}
} else {
Object.assign(data.mempool, cacheData2.mempool);
}
}
}
memPool.setMempool(data.mempool);
blocks.setBlocks(data.blocks);
} catch (e) {
logger.warn('Failed to parse mempoool and blocks cache. Skipping...');
}
}
}

View File

@@ -1,25 +1,26 @@
import config from '../config';
import { MempoolBlock } from '../mempool.interfaces';
import projectedBlocks from './mempool-blocks';
class FeeApi {
constructor() { }
defaultFee = config.MEMPOOL.NETWORK === 'liquid' ? 0.1 : 1;
public getRecommendedFee() {
const pBlocks = projectedBlocks.getMempoolBlocks();
if (!pBlocks.length) {
return {
'fastestFee': 0,
'halfHourFee': 0,
'hourFee': 0,
'fastestFee': this.defaultFee,
'halfHourFee': this.defaultFee,
'hourFee': this.defaultFee,
};
}
let firstMedianFee = Math.ceil(pBlocks[0].medianFee);
if (pBlocks.length === 1 && pBlocks[0].blockVSize <= 500000) {
firstMedianFee = 1;
}
const secondMedianFee = pBlocks[1] ? Math.ceil(pBlocks[1].medianFee) : firstMedianFee;
const thirdMedianFee = pBlocks[2] ? Math.ceil(pBlocks[2].medianFee) : secondMedianFee;
const firstMedianFee = this.optimizeMedianFee(pBlocks[0], pBlocks[1]);
const secondMedianFee = pBlocks[1] ? this.optimizeMedianFee(pBlocks[1], pBlocks[2], firstMedianFee) : this.defaultFee;
const thirdMedianFee = pBlocks[2] ? this.optimizeMedianFee(pBlocks[2], pBlocks[3], secondMedianFee) : this.defaultFee;
return {
'fastestFee': firstMedianFee,
@@ -27,6 +28,18 @@ class FeeApi {
'hourFee': thirdMedianFee,
};
}
private optimizeMedianFee(pBlock: MempoolBlock, nextBlock: MempoolBlock | undefined, previousFee?: number): number {
const useFee = previousFee ? (pBlock.medianFee + previousFee) / 2 : pBlock.medianFee;
if (pBlock.blockVSize <= 500000) {
return this.defaultFee;
}
if (pBlock.blockVSize <= 950000 && nextBlock) {
const multiplier = (pBlock.blockVSize - 500000) / 500000;
return Math.max(Math.round(useFee * multiplier), this.defaultFee);
}
return Math.round(useFee);
}
}
export default new FeeApi();

View File

@@ -1,31 +1,42 @@
import * as request from 'request';
import logger from '../logger';
import axios from 'axios';
import { IConversionRates } from '../mempool.interfaces';
class FiatConversion {
private tickers = {
'BTCUSD': {
'USD': 4110.78
},
private conversionRates: IConversionRates = {
'USD': 0
};
private ratesChangedCallback: ((rates: IConversionRates) => void) | undefined;
constructor() { }
public setProgressChangedCallback(fn: (rates: IConversionRates) => void) {
this.ratesChangedCallback = fn;
}
public startService() {
console.log('Starting currency rates service');
logger.info('Starting currency rates service');
setInterval(this.updateCurrency.bind(this), 1000 * 60 * 60);
this.updateCurrency();
}
public getTickers() {
return this.tickers;
public getConversionRates() {
return this.conversionRates;
}
private updateCurrency() {
request('https://api.opennode.co/v1/rates', { json: true }, (err, res, body) => {
if (err) { return console.log(err); }
if (body && body.data) {
this.tickers = body.data;
private async updateCurrency(): Promise<void> {
try {
const response = await axios.get('https://price.bisq.wiz.biz/getAllMarketPrices', { timeout: 10000 });
const usd = response.data.data.find((item: any) => item.currencyCode === 'USD');
this.conversionRates = {
'USD': usd.price,
};
if (this.ratesChangedCallback) {
this.ratesChangedCallback(this.conversionRates);
}
});
} catch (e) {
logger.err('Error updating fiat conversion rates: ' + e);
}
}
}

View File

@@ -0,0 +1,32 @@
import { ILoadingIndicators } from '../mempool.interfaces';
class LoadingIndicators {
private loadingIndicators: ILoadingIndicators = {
'mempool': 0,
};
private progressChangedCallback: ((loadingIndicators: ILoadingIndicators) => void) | undefined;
constructor() { }
public setProgressChangedCallback(fn: (loadingIndicators: ILoadingIndicators) => void) {
this.progressChangedCallback = fn;
}
public setProgress(name: string, progressPercent: number) {
const newProgress = Math.round(progressPercent);
if (newProgress >= 100) {
delete this.loadingIndicators[name];
} else {
this.loadingIndicators[name] = newProgress;
}
if (this.progressChangedCallback) {
this.progressChangedCallback(this.loadingIndicators);
}
}
public getLoadingIndicators() {
return this.loadingIndicators;
}
}
export default new LoadingIndicators();

View File

@@ -0,0 +1,38 @@
interface ICache {
type: string;
id: string;
expires: Date;
data: any;
}
class MemoryCache {
private cache: ICache[] = [];
constructor() {
setInterval(this.cleanup.bind(this), 1000);
}
public set(type: string, id: string, data: any, secondsExpiry: number) {
const expiry = new Date();
expiry.setSeconds(expiry.getSeconds() + secondsExpiry);
this.cache.push({
type: type,
id: id,
data: data,
expires: expiry,
});
}
public get<T>(type: string, id: string): T | null {
const found = this.cache.find((cache) => cache.type === type && cache.id === id);
if (found) {
return found.data;
}
return null;
}
private cleanup() {
this.cache = this.cache.filter((cache) => cache.expires < (new Date()));
}
}
export default new MemoryCache();

View File

@@ -1,8 +1,8 @@
const config = require('../../mempool-config.json');
import { MempoolBlock, TransactionExtended, MempoolBlockWithTransactions } from '../interfaces';
import { MempoolBlock, TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
import { Common } from './common';
class MempoolBlocks {
private static DEFAULT_PROJECTED_BLOCKS_AMOUNT = 8;
private mempoolBlocks: MempoolBlockWithTransactions[] = [];
constructor() {}
@@ -43,7 +43,7 @@ class MempoolBlocks {
let blockSize = 0;
let transactions: TransactionExtended[] = [];
transactionsSorted.forEach((tx) => {
if (blockVSize + tx.vsize <= 1000000 || mempoolBlocks.length === config.DEFAULT_PROJECTED_BLOCKS_AMOUNT - 1) {
if (blockVSize + tx.vsize <= 1000000 || mempoolBlocks.length === MempoolBlocks.DEFAULT_PROJECTED_BLOCKS_AMOUNT - 1) {
blockVSize += tx.vsize;
blockSize += tx.size;
transactions.push(tx);

View File

@@ -1,12 +1,20 @@
const config = require('../../mempool-config.json');
import bitcoinApi from './bitcoin/electrs-api';
import { MempoolInfo, TransactionExtended, Transaction, VbytesPerSecond } from '../interfaces';
import config from '../config';
import bitcoinApi from './bitcoin/bitcoin-api-factory';
import { TransactionExtended, VbytesPerSecond } from '../mempool.interfaces';
import logger from '../logger';
import { Common } from './common';
import transactionUtils from './transaction-utils';
import { IBitcoinApi } from './bitcoin/bitcoin-api.interface';
import bitcoinBaseApi from './bitcoin/bitcoin-base.api';
import loadingIndicators from './loading-indicators';
class Mempool {
private static WEBSOCKET_REFRESH_RATE_MS = 10000;
private inSync: boolean = false;
private mempoolCache: { [txId: string]: TransactionExtended } = {};
private mempoolInfo: MempoolInfo = { size: 0, bytes: 0 };
private mempoolChangedCallback: ((newMempool: { [txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0,
maxmempool: 0, mempoolminfee: 0, minrelaytxfee: 0 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
deletedTransactions: TransactionExtended[]) => void) | undefined;
private txPerSecondArray: number[] = [];
@@ -15,15 +23,25 @@ class Mempool {
private vBytesPerSecondArray: VbytesPerSecond[] = [];
private vBytesPerSecond: number = 0;
private mempoolProtection = 0;
private latestTransactions: any[] = [];
constructor() {
setInterval(this.updateTxPerSecond.bind(this), 1000);
}
public isInSync() {
public isInSync(): boolean {
return this.inSync;
}
public setOutOfSync(): void {
this.inSync = false;
loadingIndicators.setProgress('mempool', 99);
}
public getLatestTransactions() {
return this.latestTransactions;
}
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; },
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => void) {
this.mempoolChangedCallback = fn;
@@ -40,15 +58,11 @@ class Mempool {
}
}
public async updateMemPoolInfo() {
try {
this.mempoolInfo = await bitcoinApi.getMempoolInfo();
} catch (err) {
console.log('Error getMempoolInfo', err);
}
public async $updateMemPoolInfo() {
this.mempoolInfo = await bitcoinBaseApi.$getMempoolInfo();
}
public getMempoolInfo(): MempoolInfo | undefined {
public getMempoolInfo(): IBitcoinApi.MempoolInfo | undefined {
return this.mempoolInfo;
}
@@ -63,8 +77,9 @@ class Mempool {
public getFirstSeenForTransactions(txIds: string[]): number[] {
const txTimes: number[] = [];
txIds.forEach((txId: string) => {
if (this.mempoolCache[txId]) {
txTimes.push(this.mempoolCache[txId].firstSeen);
const tx = this.mempoolCache[txId];
if (tx && tx.firstSeen) {
txTimes.push(tx.firstSeen);
} else {
txTimes.push(0);
}
@@ -72,121 +87,114 @@ class Mempool {
return txTimes;
}
public async getTransactionExtended(txId: string): Promise<TransactionExtended | false> {
try {
const transaction: Transaction = await bitcoinApi.getRawTransaction(txId);
return Object.assign({
vsize: transaction.weight / 4,
feePerVsize: (transaction.fee || 0) / (transaction.weight / 4),
firstSeen: Math.round((new Date().getTime() / 1000)),
}, transaction);
} catch (e) {
console.log(txId + ' not found');
return false;
}
}
public async updateMempool() {
console.log('Updating mempool');
public async $updateMempool() {
logger.debug('Updating mempool');
const start = new Date().getTime();
let hasChange: boolean = false;
const currentMempoolSize = Object.keys(this.mempoolCache).length;
let txCount = 0;
try {
const transactions = await bitcoinApi.getRawMempool();
const diff = transactions.length - currentMempoolSize;
const newTransactions: TransactionExtended[] = [];
const transactions = await bitcoinApi.$getRawMempool();
const diff = transactions.length - currentMempoolSize;
const newTransactions: TransactionExtended[] = [];
for (const txid of transactions) {
if (!this.mempoolCache[txid]) {
const transaction = await this.getTransactionExtended(txid);
if (transaction) {
this.mempoolCache[txid] = transaction;
txCount++;
if (this.inSync) {
this.txPerSecondArray.push(new Date().getTime());
this.vBytesPerSecondArray.push({
unixTime: new Date().getTime(),
vSize: transaction.vsize,
});
}
hasChange = true;
if (diff > 0) {
console.log('Fetched transaction ' + txCount + ' / ' + diff);
} else {
console.log('Fetched transaction ' + txCount);
}
newTransactions.push(transaction);
} else {
console.log('Error finding transaction in mempool.');
}
}
if ((new Date().getTime()) - start > config.MEMPOOL_REFRESH_RATE_MS * 10) {
break;
}
}
// Prevent mempool from clear on bitcoind restart by delaying the deletion
if (this.mempoolProtection === 0 && transactions.length / currentMempoolSize <= 0.80) {
this.mempoolProtection = 1;
this.inSync = false;
console.log('Mempool clear protection triggered.');
setTimeout(() => {
this.mempoolProtection = 2;
console.log('Mempool clear protection resumed.');
}, 1000 * 60 * 2);
}
let newMempool = {};
const deletedTransactions: TransactionExtended[] = [];
if (this.mempoolProtection !== 1) {
this.mempoolProtection = 0;
// Index object for faster search
const transactionsObject = {};
transactions.forEach((txId) => transactionsObject[txId] = true);
// Replace mempool to separate deleted transactions
for (const tx in this.mempoolCache) {
if (transactionsObject[tx]) {
newMempool[tx] = this.mempoolCache[tx];
} else {
deletedTransactions.push(this.mempoolCache[tx]);
}
}
} else {
newMempool = this.mempoolCache;
}
if (!this.inSync && transactions.length === Object.keys(newMempool).length) {
this.inSync = true;
console.log('The mempool is now in sync!');
}
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.mempoolCache = newMempool;
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
}
const end = new Date().getTime();
const time = end - start;
console.log(`New mempool size: ${Object.keys(newMempool).length} Change: ${diff}`);
console.log('Mempool updated in ' + time / 1000 + ' seconds');
} catch (err) {
console.log('getRawMempool error.', err);
if (!this.inSync) {
loadingIndicators.setProgress('mempool', Object.keys(this.mempoolCache).length / transactions.length * 100);
}
for (const txid of transactions) {
if (!this.mempoolCache[txid]) {
try {
const transaction = await transactionUtils.$getTransactionExtended(txid);
this.mempoolCache[txid] = transaction;
txCount++;
if (this.inSync) {
this.txPerSecondArray.push(new Date().getTime());
this.vBytesPerSecondArray.push({
unixTime: new Date().getTime(),
vSize: transaction.vsize,
});
}
hasChange = true;
if (diff > 0) {
logger.debug('Fetched transaction ' + txCount + ' / ' + diff);
} else {
logger.debug('Fetched transaction ' + txCount);
}
newTransactions.push(transaction);
} catch (e) {
logger.debug('Error finding transaction in mempool: ' + e.message || e);
}
}
if ((new Date().getTime()) - start > Mempool.WEBSOCKET_REFRESH_RATE_MS) {
break;
}
}
// Prevent mempool from clear on bitcoind restart by delaying the deletion
if (this.mempoolProtection === 0
&& currentMempoolSize > 20000
&& transactions.length / currentMempoolSize <= 0.80
) {
this.mempoolProtection = 1;
this.inSync = false;
logger.warn(`Mempool clear protection triggered because transactions.length: ${transactions.length} and currentMempoolSize: ${currentMempoolSize}.`);
setTimeout(() => {
this.mempoolProtection = 2;
logger.warn('Mempool clear protection resumed.');
}, 1000 * 60 * config.MEMPOOL.CLEAR_PROTECTION_MINUTES);
}
let newMempool = {};
const deletedTransactions: TransactionExtended[] = [];
if (this.mempoolProtection !== 1) {
this.mempoolProtection = 0;
// Index object for faster search
const transactionsObject = {};
transactions.forEach((txId) => transactionsObject[txId] = true);
// Replace mempool to separate deleted transactions
for (const tx in this.mempoolCache) {
if (transactionsObject[tx]) {
newMempool[tx] = this.mempoolCache[tx];
} else {
deletedTransactions.push(this.mempoolCache[tx]);
}
}
} else {
newMempool = this.mempoolCache;
}
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
if (!this.inSync && transactions.length === Object.keys(newMempool).length) {
this.inSync = true;
logger.info('The mempool is now in sync!');
loadingIndicators.setProgress('mempool', 100);
}
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.mempoolCache = newMempool;
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
}
const end = new Date().getTime();
const time = end - start;
logger.debug(`New mempool size: ${Object.keys(newMempool).length} Change: ${diff}`);
logger.debug('Mempool updated in ' + time / 1000 + ' seconds');
}
private updateTxPerSecond() {
const nowMinusTimeSpan = new Date().getTime() - (1000 * config.TX_PER_SECOND_SPAN_SECONDS);
const nowMinusTimeSpan = new Date().getTime() - (1000 * config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD);
this.txPerSecondArray = this.txPerSecondArray.filter((unixTime) => unixTime > nowMinusTimeSpan);
this.txPerSecond = this.txPerSecondArray.length / config.TX_PER_SECOND_SPAN_SECONDS || 0;
this.txPerSecond = this.txPerSecondArray.length / config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD || 0;
this.vBytesPerSecondArray = this.vBytesPerSecondArray.filter((data) => data.unixTime > nowMinusTimeSpan);
if (this.vBytesPerSecondArray.length) {
this.vBytesPerSecond = Math.round(
this.vBytesPerSecondArray.map((data) => data.vSize).reduce((a, b) => a + b) / config.TX_PER_SECOND_SPAN_SECONDS
this.vBytesPerSecondArray.map((data) => data.vSize).reduce((a, b) => a + b) / config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD
);
}
}

View File

@@ -1,11 +1,16 @@
import memPool from './mempool';
import { DB } from '../database';
import logger from '../logger';
import { Statistic, TransactionExtended, OptimizedStatistic } from '../interfaces';
import { Statistic, TransactionExtended, OptimizedStatistic } from '../mempool.interfaces';
class Statistics {
protected intervalTimer: NodeJS.Timer | undefined;
protected newStatisticsEntryCallback: ((stats: OptimizedStatistic) => void) | undefined;
protected queryTimeout = 120000;
protected cache: { [date: string]: OptimizedStatistic[] } = {
'24h': [], '1w': [], '1m': [], '3m': [], '6m': [], '1y': [],
};
public setNewStatisticsEntryCallback(fn: (stats: OptimizedStatistic) => void) {
this.newStatisticsEntryCallback = fn;
@@ -14,7 +19,7 @@ class Statistics {
constructor() { }
public startStatistics(): void {
console.log('Starting statistics service');
logger.info('Starting statistics service');
const now = new Date();
const nextInterval = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(),
@@ -24,20 +29,37 @@ class Statistics {
setTimeout(() => {
this.runStatistics();
this.intervalTimer = setInterval(() => {
if (!memPool.isInSync()) {
return;
}
this.runStatistics();
}, 1 * 60 * 1000);
}, difference);
this.createCache();
setInterval(this.createCache.bind(this), 600000);
}
public getCache() {
return this.cache;
}
private async createCache() {
this.cache['24h'] = await this.$list24H();
this.cache['1w'] = await this.$list1W();
this.cache['1m'] = await this.$list1M();
this.cache['3m'] = await this.$list3M();
this.cache['6m'] = await this.$list6M();
this.cache['1y'] = await this.$list1Y();
logger.debug('Statistics cache created');
}
private async runStatistics(): Promise<void> {
if (!memPool.isInSync()) {
return;
}
const currentMempool = memPool.getMempool();
const txPerSecond = memPool.getTxPerSecond();
const vBytesPerSecond = memPool.getVBytesPerSecond();
console.log('Running statistics');
logger.debug('Running statistics');
let memPoolArray: TransactionExtended[] = [];
for (const i in currentMempool) {
@@ -233,53 +255,52 @@ class Statistics {
connection.release();
return result.insertId;
} catch (e) {
console.log('$create() error', e);
logger.err('$create() error' + e.message || e);
}
}
private getQueryForDays(days: number, groupBy: number) {
private getQueryForDays(div: number) {
return `SELECT id, added, unconfirmed_transactions,
AVG(tx_per_second) AS tx_per_second,
AVG(vbytes_per_second) AS vbytes_per_second,
AVG(vsize_1) AS vsize_1,
AVG(vsize_2) AS vsize_2,
AVG(vsize_3) AS vsize_3,
AVG(vsize_4) AS vsize_4,
AVG(vsize_5) AS vsize_5,
AVG(vsize_6) AS vsize_6,
AVG(vsize_8) AS vsize_8,
AVG(vsize_10) AS vsize_10,
AVG(vsize_12) AS vsize_12,
AVG(vsize_15) AS vsize_15,
AVG(vsize_20) AS vsize_20,
AVG(vsize_30) AS vsize_30,
AVG(vsize_40) AS vsize_40,
AVG(vsize_50) AS vsize_50,
AVG(vsize_60) AS vsize_60,
AVG(vsize_70) AS vsize_70,
AVG(vsize_80) AS vsize_80,
AVG(vsize_90) AS vsize_90,
AVG(vsize_100) AS vsize_100,
AVG(vsize_125) AS vsize_125,
AVG(vsize_150) AS vsize_150,
AVG(vsize_175) AS vsize_175,
AVG(vsize_200) AS vsize_200,
AVG(vsize_250) AS vsize_250,
AVG(vsize_300) AS vsize_300,
AVG(vsize_350) AS vsize_350,
AVG(vsize_400) AS vsize_400,
AVG(vsize_500) AS vsize_500,
AVG(vsize_600) AS vsize_600,
AVG(vsize_700) AS vsize_700,
AVG(vsize_800) AS vsize_800,
AVG(vsize_900) AS vsize_900,
AVG(vsize_1000) AS vsize_1000,
AVG(vsize_1200) AS vsize_1200,
AVG(vsize_1400) AS vsize_1400,
AVG(vsize_1600) AS vsize_1600,
AVG(vsize_1800) AS vsize_1800,
AVG(vsize_2000) AS vsize_2000 FROM statistics GROUP BY UNIX_TIMESTAMP(added) DIV ${groupBy} ORDER BY id DESC LIMIT ${days}`;
tx_per_second,
vbytes_per_second,
vsize_1,
vsize_2,
vsize_3,
vsize_4,
vsize_5,
vsize_6,
vsize_8,
vsize_10,
vsize_12,
vsize_15,
vsize_20,
vsize_30,
vsize_40,
vsize_50,
vsize_60,
vsize_70,
vsize_80,
vsize_90,
vsize_100,
vsize_125,
vsize_150,
vsize_175,
vsize_200,
vsize_250,
vsize_300,
vsize_350,
vsize_400,
vsize_500,
vsize_600,
vsize_700,
vsize_800,
vsize_900,
vsize_1000,
vsize_1200,
vsize_1400,
vsize_1600,
vsize_1800,
vsize_2000 FROM statistics GROUP BY UNIX_TIMESTAMP(added) DIV ${div} ORDER BY id DESC LIMIT 480`;
}
public async $get(id: number): Promise<OptimizedStatistic | undefined> {
@@ -292,7 +313,7 @@ class Statistics {
return this.mapStatisticToOptimizedStatistic([rows[0]])[0];
}
} catch (e) {
console.log('$list2H() error', e);
logger.err('$list2H() error' + e.message || e);
}
}
@@ -300,11 +321,11 @@ class Statistics {
try {
const connection = await DB.pool.getConnection();
const query = `SELECT * FROM statistics ORDER BY id DESC LIMIT 120`;
const [rows] = await connection.query<any>(query);
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
connection.release();
return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) {
console.log('$list2H() error', e);
logger.err('$list2H() error' + e.message || e);
return [];
}
}
@@ -312,11 +333,12 @@ class Statistics {
public async $list24H(): Promise<OptimizedStatistic[]> {
try {
const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(120, 720);
const [rows] = await connection.query<any>(query);
const query = this.getQueryForDays(180);
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
connection.release();
return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) {
logger.err('$list24h() error' + e.message || e);
return [];
}
}
@@ -324,12 +346,12 @@ class Statistics {
public async $list1W(): Promise<OptimizedStatistic[]> {
try {
const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(120, 5040);
const [rows] = await connection.query<any>(query);
const query = this.getQueryForDays(1260);
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
connection.release();
return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) {
console.log('$list1W() error', e);
logger.err('$list1W() error' + e);
return [];
}
}
@@ -337,12 +359,12 @@ class Statistics {
public async $list1M(): Promise<OptimizedStatistic[]> {
try {
const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(120, 20160);
const [rows] = await connection.query<any>(query);
const query = this.getQueryForDays(5040);
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
connection.release();
return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) {
console.log('$list1M() error', e);
logger.err('$list1M() error' + e);
return [];
}
}
@@ -350,12 +372,12 @@ class Statistics {
public async $list3M(): Promise<OptimizedStatistic[]> {
try {
const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(120, 60480);
const [rows] = await connection.query<any>(query);
const query = this.getQueryForDays(15120);
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
connection.release();
return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) {
console.log('$list3M() error', e);
logger.err('$list3M() error' + e);
return [];
}
}
@@ -363,12 +385,12 @@ class Statistics {
public async $list6M(): Promise<OptimizedStatistic[]> {
try {
const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(120, 120960);
const [rows] = await connection.query<any>(query);
const query = this.getQueryForDays(30240);
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
connection.release();
return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) {
console.log('$list6M() error', e);
logger.err('$list6M() error' + e);
return [];
}
}
@@ -376,12 +398,12 @@ class Statistics {
public async $list1Y(): Promise<OptimizedStatistic[]> {
try {
const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(120, 241920);
const [rows] = await connection.query<any>(query);
const query = this.getQueryForDays(60480);
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
connection.release();
return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) {
console.log('$list6M() error', e);
logger.err('$list6M() error' + e);
return [];
}
}

View File

@@ -0,0 +1,40 @@
import bitcoinApi from './bitcoin/bitcoin-api-factory';
import logger from '../logger';
import { TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
import { IEsploraApi } from './bitcoin/esplora-api.interface';
class TransactionUtils {
constructor() { }
public stripCoinbaseTransaction(tx: TransactionExtended): TransactionMinerInfo {
return {
vin: [{
scriptsig: tx.vin[0].scriptsig || tx.vin[0]['coinbase']
}],
vout: tx.vout
.map((vout) => ({
scriptpubkey_address: vout.scriptpubkey_address,
value: vout.value
}))
.filter((vout) => vout.value)
};
}
public async $getTransactionExtended(txId: string, addPrevouts = false): Promise<TransactionExtended> {
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts);
return this.extendTransaction(transaction);
}
private extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
const transactionExtended: TransactionExtended = Object.assign({
vsize: Math.round(transaction.weight / 4),
feePerVsize: Math.max(1, (transaction.fee || 0) / (transaction.weight / 4)),
}, transaction);
if (!transaction.status.confirmed) {
transactionExtended.firstSeen = Math.round((new Date().getTime() / 1000));
}
return transactionExtended;
}
}
export default new TransactionUtils();

View File

@@ -1,13 +1,16 @@
const config = require('../../mempool-config.json');
import logger from '../logger';
import * as WebSocket from 'ws';
import { Block, TransactionExtended, WebsocketResponse, MempoolBlock, OptimizedStatistic } from '../interfaces';
import { BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock,
OptimizedStatistic, ILoadingIndicators, IConversionRates } from '../mempool.interfaces';
import blocks from './blocks';
import memPool from './mempool';
import backendInfo from './backend-info';
import mempoolBlocks from './mempool-blocks';
import fiatConversion from './fiat-conversion';
import { Common } from './common';
import loadingIndicators from './loading-indicators';
import config from '../config';
import transactionUtils from './transaction-utils';
class WebsocketHandler {
private wss: WebSocket.Server | undefined;
@@ -30,6 +33,7 @@ class WebsocketHandler {
}
this.wss.on('connection', (client: WebSocket) => {
client.on('error', logger.info);
client.on('message', (message: string) => {
try {
const parsedMessage: WebsocketResponse = JSON.parse(message);
@@ -77,36 +81,91 @@ class WebsocketHandler {
}
if (parsedMessage.action === 'init') {
const _blocks = blocks.getBlocks();
const _blocks = blocks.getBlocks().slice(-8);
if (!_blocks) {
return;
}
client.send(JSON.stringify({
'mempoolInfo': memPool.getMempoolInfo(),
'vBytesPerSecond': memPool.getVBytesPerSecond(),
'blocks': _blocks.slice(Math.max(_blocks.length - config.INITIAL_BLOCK_AMOUNT, 0)),
'conversions': fiatConversion.getTickers()['BTCUSD'],
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
'git-commit': backendInfo.gitCommitHash,
'hostname': backendInfo.hostname,
...this.extraInitProperties
}));
client.send(JSON.stringify(this.getInitData(_blocks)));
}
if (parsedMessage.action === 'ping') {
response['pong'] = true;
}
if (parsedMessage['track-donation'] && parsedMessage['track-donation'].length === 22) {
client['track-donation'] = parsedMessage['track-donation'];
}
if (Object.keys(response).length) {
client.send(JSON.stringify(response));
}
} catch (e) {
console.log(e);
logger.debug('Error parsing websocket message: ' + e.message || e);
}
});
});
}
handleNewDonation(id: string) {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
}
this.wss.clients.forEach((client: WebSocket) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
if (client['track-donation'] === id) {
client.send(JSON.stringify({ donationConfirmed: true }));
}
});
}
handleLoadingChanged(indicators: ILoadingIndicators) {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
}
this.wss.clients.forEach((client: WebSocket) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
client.send(JSON.stringify({ loadingIndicators: indicators }));
});
}
handleNewConversionRates(conversionRates: IConversionRates) {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
}
this.wss.clients.forEach((client: WebSocket) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
client.send(JSON.stringify({ conversions: conversionRates }));
});
}
getInitData(_blocks?: BlockExtended[]) {
if (!_blocks) {
_blocks = blocks.getBlocks().slice(-8);
}
return {
'mempoolInfo': memPool.getMempoolInfo(),
'vBytesPerSecond': memPool.getVBytesPerSecond(),
'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(),
'blocks': _blocks,
'conversions': fiatConversion.getConversionRates(),
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
'transactions': memPool.getLatestTransactions(),
'git-commit': backendInfo.gitCommitHash,
'hostname': backendInfo.hostname,
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
...this.extraInitProperties
};
}
handleNewStatistic(stats: OptimizedStatistic) {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
@@ -139,7 +198,7 @@ class WebsocketHandler {
const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
this.wss.clients.forEach((client: WebSocket) => {
this.wss.clients.forEach(async (client: WebSocket) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
@@ -149,6 +208,7 @@ class WebsocketHandler {
if (client['want-stats']) {
response['mempoolInfo'] = mempoolInfo;
response['vBytesPerSecond'] = vBytesPerSecond;
response['transactions'] = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
}
if (client['want-mempool-blocks']) {
@@ -158,7 +218,16 @@ class WebsocketHandler {
if (client['track-mempool-tx']) {
const tx = newTransactions.find((t) => t.txid === client['track-mempool-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;
}
client['track-mempool-tx'] = null;
}
}
@@ -166,17 +235,35 @@ class WebsocketHandler {
if (client['track-address']) {
const foundTransactions: TransactionExtended[] = [];
newTransactions.forEach((tx) => {
for (const tx of newTransactions) {
const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_address === client['track-address']);
if (someVin) {
foundTransactions.push(tx);
if (config.MEMPOOL.BACKEND !== 'esplora') {
try {
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
foundTransactions.push(fullTx);
} catch (e) {
logger.debug('Error finding transaction in mempool: ' + e.message || e);
}
} else {
foundTransactions.push(tx);
}
return;
}
const someVout = tx.vout.some((vout) => vout.scriptpubkey_address === client['track-address']);
if (someVout) {
foundTransactions.push(tx);
if (config.MEMPOOL.BACKEND !== 'esplora') {
try {
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
foundTransactions.push(fullTx);
} catch (e) {
logger.debug('Error finding transaction in mempool: ' + e.message || e);
}
} else {
foundTransactions.push(tx);
}
}
});
}
if (foundTransactions.length) {
response['address-transactions'] = foundTransactions;
@@ -215,7 +302,17 @@ class WebsocketHandler {
if (client['track-tx'] && rbfTransactions[client['track-tx']]) {
for (const rbfTransaction in rbfTransactions) {
if (client['track-tx'] === rbfTransaction) {
response['rbfTransaction'] = rbfTransactions[rbfTransaction];
const rbfTx = rbfTransactions[rbfTransaction];
if (config.MEMPOOL.BACKEND !== 'esplora') {
try {
const fullTx = await transactionUtils.$getTransactionExtended(rbfTransaction, true);
response['rbfTransaction'] = fullTx;
} catch (e) {
logger.debug('Error finding transaction in mempool: ' + e.message || e);
}
} else {
response['rbfTransaction'] = rbfTx;
}
break;
}
}
@@ -227,7 +324,7 @@ class WebsocketHandler {
});
}
handleNewBlock(block: Block, txIds: string[], transactions: TransactionExtended[]) {
handleNewBlock(block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
}
@@ -270,6 +367,7 @@ class WebsocketHandler {
const response = {
'block': block,
'mempoolInfo': memPool.getMempoolInfo(),
'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(),
};
if (mBlocks && client['want-mempool-blocks']) {

146
backend/src/config.ts Normal file
View File

@@ -0,0 +1,146 @@
const configFile = require('../mempool-config.json');
interface IConfig {
MEMPOOL: {
NETWORK: 'mainnet' | 'testnet' | 'liquid';
BACKEND: 'esplora' | 'electrum' | 'none';
HTTP_PORT: number;
SPAWN_CLUSTER_PROCS: number;
API_URL_PREFIX: string;
POLL_RATE_MS: number;
CACHE_DIR: string;
CLEAR_PROTECTION_MINUTES: number;
};
ESPLORA: {
REST_API_URL: string;
};
ELECTRUM: {
HOST: string;
PORT: number;
TLS_ENABLED: boolean;
};
CORE_RPC: {
HOST: string;
PORT: number;
USERNAME: string;
PASSWORD: string;
};
CORE_RPC_MINFEE: {
ENABLED: boolean;
HOST: string;
PORT: number;
USERNAME: string;
PASSWORD: string;
};
DATABASE: {
ENABLED: boolean;
HOST: string,
PORT: number;
DATABASE: string;
USERNAME: string;
PASSWORD: string;
};
STATISTICS: {
ENABLED: boolean;
TX_PER_SECOND_SAMPLE_PERIOD: number;
};
BISQ_BLOCKS: {
ENABLED: boolean;
DATA_PATH: string;
};
BISQ_MARKETS: {
ENABLED: boolean;
DATA_PATH: string;
};
}
const defaults: IConfig = {
'MEMPOOL': {
'NETWORK': 'mainnet',
'BACKEND': 'none',
'HTTP_PORT': 8999,
'SPAWN_CLUSTER_PROCS': 0,
'API_URL_PREFIX': '/api/v1/',
'POLL_RATE_MS': 2000,
'CACHE_DIR': './cache',
'CLEAR_PROTECTION_MINUTES': 20,
},
'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000',
},
'ELECTRUM': {
'HOST': '127.0.0.1',
'PORT': 3306,
'TLS_ENABLED': true,
},
'CORE_RPC': {
'HOST': '127.0.0.1',
'PORT': 8332,
'USERNAME': 'mempool',
'PASSWORD': 'mempool'
},
'CORE_RPC_MINFEE': {
'ENABLED': false,
'HOST': '127.0.0.1',
'PORT': 8332,
'USERNAME': 'mempool',
'PASSWORD': 'mempool'
},
'DATABASE': {
'ENABLED': true,
'HOST': '127.0.0.1',
'PORT': 3306,
'DATABASE': 'mempool',
'USERNAME': 'mempool',
'PASSWORD': 'mempool'
},
'STATISTICS': {
'ENABLED': true,
'TX_PER_SECOND_SAMPLE_PERIOD': 150
},
'BISQ_BLOCKS': {
'ENABLED': false,
'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db/json'
},
'BISQ_MARKETS': {
'ENABLED': false,
'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db'
},
};
class Config implements IConfig {
MEMPOOL: IConfig['MEMPOOL'];
ESPLORA: IConfig['ESPLORA'];
ELECTRUM: IConfig['ELECTRUM'];
CORE_RPC: IConfig['CORE_RPC'];
CORE_RPC_MINFEE: IConfig['CORE_RPC_MINFEE'];
DATABASE: IConfig['DATABASE'];
STATISTICS: IConfig['STATISTICS'];
BISQ_BLOCKS: IConfig['BISQ_BLOCKS'];
BISQ_MARKETS: IConfig['BISQ_MARKETS'];
constructor() {
const configs = this.merge(configFile, defaults);
this.MEMPOOL = configs.MEMPOOL;
this.ESPLORA = configs.ESPLORA;
this.ELECTRUM = configs.ELECTRUM;
this.CORE_RPC = configs.CORE_RPC;
this.CORE_RPC_MINFEE = configs.CORE_RPC_MINFEE;
this.DATABASE = configs.DATABASE;
this.STATISTICS = configs.STATISTICS;
this.BISQ_BLOCKS = configs.BISQ_BLOCKS;
this.BISQ_MARKETS = configs.BISQ_MARKETS;
}
merge = (...objects: object[]): IConfig => {
// @ts-ignore
return objects.reduce((prev, next) => {
Object.keys(prev).forEach(key => {
next[key] = { ...next[key], ...prev[key] };
});
return next;
});
}
}
export default new Config();

View File

@@ -1,13 +1,14 @@
const config = require('../mempool-config.json');
import config from './config';
import { createPool } from 'mysql2/promise';
import logger from './logger';
export class DB {
static pool = createPool({
host: config.DB_HOST,
port: config.DB_PORT,
database: config.DB_DATABASE,
user: config.DB_USER,
password: config.DB_PASSWORD,
host: config.DATABASE.HOST,
port: config.DATABASE.PORT,
database: config.DATABASE.DATABASE,
user: config.DATABASE.USERNAME,
password: config.DATABASE.PASSWORD,
connectionLimit: 10,
supportBigNumbers: true,
});
@@ -16,11 +17,10 @@ export class DB {
export async function checkDbConnection() {
try {
const connection = await DB.pool.getConnection();
console.log('Database connection established.');
logger.info('Database connection established.');
connection.release();
} catch (e) {
console.log('Could not connect to database.');
console.log(e);
logger.err('Could not connect to database: ' + e.message || e);
process.exit(1);
}
}

View File

@@ -1,13 +1,12 @@
const config = require('../mempool-config.json');
import { Express, Request, Response, NextFunction } from 'express';
import * as fs from 'fs';
import * as express from 'express';
import * as compression from 'compression';
import * as http from 'http';
import * as https from 'https';
import * as WebSocket from 'ws';
import * as cluster from 'cluster';
import axios from 'axios';
import { checkDbConnection } from './database';
import config from './config';
import routes from './routes';
import blocks from './api/blocks';
import memPool from './api/mempool';
@@ -15,96 +14,218 @@ import diskCache from './api/disk-cache';
import statistics from './api/statistics';
import websocketHandler from './api/websocket-handler';
import fiatConversion from './api/fiat-conversion';
import bisq from './api/bisq';
import bisq from './api/bisq/bisq';
import bisqMarkets from './api/bisq/markets';
import logger from './logger';
import backendInfo from './api/backend-info';
import loadingIndicators from './api/loading-indicators';
import mempool from './api/mempool';
class Server {
wss: WebSocket.Server;
server: https.Server | http.Server;
app: Express;
private wss: WebSocket.Server | undefined;
private server: http.Server | undefined;
private app: Express;
private currentBackendRetryInterval = 5;
constructor() {
this.app = express();
if (!config.MEMPOOL.SPAWN_CLUSTER_PROCS) {
this.startServer();
return;
}
if (cluster.isMaster) {
logger.notice(`Mempool Server (Master) is running on port ${config.MEMPOOL.HTTP_PORT} (${backendInfo.getShortCommitHash()})`);
const numCPUs = config.MEMPOOL.SPAWN_CLUSTER_PROCS;
for (let i = 0; i < numCPUs; i++) {
const env = { workerId: i };
const worker = cluster.fork(env);
worker.process['env'] = env;
}
cluster.on('exit', (worker, code, signal) => {
const workerId = worker.process['env'].workerId;
logger.warn(`Mempool Worker PID #${worker.process.pid} workerId: ${workerId} died. Restarting in 10 seconds... ${signal || code}`);
setTimeout(() => {
const env = { workerId: workerId };
const newWorker = cluster.fork(env);
newWorker.process['env'] = env;
}, 10000);
});
} else {
this.startServer(true);
}
}
async startServer(worker = false) {
logger.debug(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
this.app
.use((req: Request, res: Response, next: NextFunction) => {
res.setHeader('Access-Control-Allow-Origin', '*');
next();
})
.use(compression());
.use(express.urlencoded({ extended: true }))
.use(express.json());
if (config.SSL === true) {
const credentials = {
cert: fs.readFileSync(config.SSL_CERT_FILE_PATH),
key: fs.readFileSync(config.SSL_KEY_FILE_PATH),
};
this.server = https.createServer(credentials, this.app);
this.wss = new WebSocket.Server({ server: this.server });
} else {
this.server = http.createServer(this.app);
this.wss = new WebSocket.Server({ server: this.server });
this.server = http.createServer(this.app);
this.wss = new WebSocket.Server({ server: this.server });
diskCache.loadMempoolCache();
if (config.DATABASE.ENABLED) {
await checkDbConnection();
}
if (!config.DB_DISABLED) {
checkDbConnection();
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
statistics.startStatistics();
}
fiatConversion.startService();
this.setUpHttpApiRoutes();
this.setUpWebsocketHandling();
this.runMempoolIntervalFunctions();
this.runMainUpdateLoop();
fiatConversion.startService();
diskCache.loadMempoolCache();
if (config.BISQ_ENABLED) {
if (config.BISQ_BLOCKS.ENABLED) {
bisq.startBisqService();
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price));
blocks.setNewBlockCallback(bisq.handleNewBitcoinBlock.bind(bisq));
}
this.server.listen(config.HTTP_PORT, () => {
console.log(`Server started on port ${config.HTTP_PORT}`);
if (config.BISQ_MARKETS.ENABLED) {
bisqMarkets.startBisqService();
}
this.server.listen(config.MEMPOOL.HTTP_PORT, () => {
if (worker) {
logger.info(`Mempool Server worker #${process.pid} started`);
} else {
logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`);
}
});
}
async runMempoolIntervalFunctions() {
await memPool.updateMemPoolInfo();
await blocks.updateBlocks();
await memPool.updateMempool();
setTimeout(this.runMempoolIntervalFunctions.bind(this), config.ELECTRS_POLL_RATE_MS);
async runMainUpdateLoop() {
try {
await memPool.$updateMemPoolInfo();
await blocks.$updateBlocks();
await memPool.$updateMempool();
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
this.currentBackendRetryInterval = 5;
} catch (e) {
const loggerMsg = `runMainLoop error: ${(e.message || e)}. Retrying in ${this.currentBackendRetryInterval} sec.`;
if (this.currentBackendRetryInterval > 5) {
logger.warn(loggerMsg);
mempool.setOutOfSync();
} else {
logger.debug(loggerMsg);
}
logger.debug(JSON.stringify(e));
setTimeout(this.runMainUpdateLoop.bind(this), 1000 * this.currentBackendRetryInterval);
this.currentBackendRetryInterval *= 2;
this.currentBackendRetryInterval = Math.min(this.currentBackendRetryInterval, 60);
}
}
setUpWebsocketHandling() {
websocketHandler.setWebsocketServer(this.wss);
if (this.wss) {
websocketHandler.setWebsocketServer(this.wss);
}
websocketHandler.setupConnectionHandling();
statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler));
blocks.setNewBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
memPool.setMempoolChangedCallback(websocketHandler.handleMempoolChange.bind(websocketHandler));
fiatConversion.setProgressChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
}
setUpHttpApiRoutes() {
this.app
.get(config.API_ENDPOINT + 'transaction-times', routes.getTransactionTimes)
.get(config.API_ENDPOINT + 'fees/recommended', routes.getRecommendedFees)
.get(config.API_ENDPOINT + 'fees/mempool-blocks', routes.getMempoolBlocks)
.get(config.API_ENDPOINT + 'statistics/2h', routes.get2HStatistics)
.get(config.API_ENDPOINT + 'statistics/24h', routes.get24HStatistics.bind(routes))
.get(config.API_ENDPOINT + 'statistics/1w', routes.get1WHStatistics.bind(routes))
.get(config.API_ENDPOINT + 'statistics/1m', routes.get1MStatistics.bind(routes))
.get(config.API_ENDPOINT + 'statistics/3m', routes.get3MStatistics.bind(routes))
.get(config.API_ENDPOINT + 'statistics/6m', routes.get6MStatistics.bind(routes))
.get(config.API_ENDPOINT + 'statistics/1y', routes.get1YStatistics.bind(routes))
.get(config.API_ENDPOINT + 'backend-info', routes.getBackendInfo)
.get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', routes.getTransactionTimes)
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', routes.getRecommendedFees)
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', routes.getMempoolBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', routes.getBackendInfo)
.get(config.MEMPOOL.API_URL_PREFIX + 'init-data', routes.getInitData)
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
try {
const response = await axios.get('https://mempool.space/api/v1/donations', { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
try {
const response = await axios.get('https://mempool.space/api/v1/donations/images/' + req.params.id, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
;
if (config.BISQ_ENABLED) {
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
this.app
.get(config.API_ENDPOINT + 'bisq/stats', routes.getBisqStats)
.get(config.API_ENDPOINT + 'bisq/tx/:txId', routes.getBisqTransaction)
.get(config.API_ENDPOINT + 'bisq/block/:hash', routes.getBisqBlock)
.get(config.API_ENDPOINT + 'bisq/blocks/tip/height', routes.getBisqTip)
.get(config.API_ENDPOINT + 'bisq/blocks/:index/:length', routes.getBisqBlocks)
.get(config.API_ENDPOINT + 'bisq/address/:address', routes.getBisqAddress)
.get(config.API_ENDPOINT + 'bisq/txs/:index/:length', routes.getBisqTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2h', routes.get2HStatistics)
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/24h', routes.get24HStatistics.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1w', routes.get1WHStatistics.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1m', routes.get1MStatistics.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3m', routes.get3MStatistics.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/6m', routes.get6MStatistics.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', routes.get1YStatistics.bind(routes))
;
}
if (config.BISQ_BLOCKS.ENABLED) {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/stats', routes.getBisqStats)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/tx/:txId', routes.getBisqTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/block/:hash', routes.getBisqBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/blocks/tip/height', routes.getBisqTip)
.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))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/markets', routes.getBisqMarketMarkets.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/offers', routes.getBisqMarketOffers.bind(routes))
.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))
;
}
if (config.MEMPOOL.BACKEND !== 'esplora') {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool', routes.getMempool)
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool/txids', routes.getMempoolTxIds)
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', routes.getRecentMempoolTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', routes.getTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', routes.getTransactionStatus)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', routes.getTransactionOutspends)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', routes.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', routes.getBlockTipHeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs', routes.getBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs/:index', routes.getBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', routes.getTxIdsForBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', routes.getBlockHeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', routes.getAddress)
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', routes.getAddressTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs/chain/:txId', routes.getAddressTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', routes.getAddressPrefix)
;
}
}

View File

@@ -1,324 +0,0 @@
export interface MempoolInfo {
size: number;
bytes: number;
usage?: number;
maxmempool?: number;
mempoolminfee?: number;
minrelaytxfee?: number;
}
export interface MempoolBlock {
blockSize: number;
blockVSize: number;
nTx: number;
medianFee: number;
totalFees: number;
feeRange: number[];
}
export interface MempoolBlockWithTransactions extends MempoolBlock {
transactionIds: string[];
}
export interface Transaction {
txid: string;
version: number;
locktime: number;
fee: number;
size: number;
weight: number;
vin: Vin[];
vout: Vout[];
status: Status;
}
export interface TransactionMinerInfo {
vin: VinStrippedToScriptsig[];
vout: VoutStrippedToScriptPubkey[];
}
interface VinStrippedToScriptsig {
scriptsig: string;
}
interface VoutStrippedToScriptPubkey {
scriptpubkey_address: string | undefined;
value: number;
}
export interface TransactionExtended extends Transaction {
vsize: number;
feePerVsize: number;
firstSeen: number;
}
export interface Vin {
txid: string;
vout: number;
is_coinbase: boolean;
scriptsig: string;
scriptsig_asm: string;
inner_redeemscript_asm?: string;
inner_witnessscript_asm?: string;
sequence: any;
witness?: string[];
prevout: Vout;
// Elements
is_pegin?: boolean;
issuance?: Issuance;
}
interface Issuance {
asset_id: string;
is_reissuance: string;
asset_blinding_nonce: string;
asset_entropy: string;
contract_hash: string;
assetamount?: number;
assetamountcommitment?: string;
tokenamount?: number;
tokenamountcommitment?: string;
}
export interface Vout {
scriptpubkey: string;
scriptpubkey_asm: string;
scriptpubkey_type: string;
scriptpubkey_address: string;
value: number;
// Elements
valuecommitment?: number;
asset?: string;
pegout?: Pegout;
}
interface Pegout {
genesis_hash: string;
scriptpubkey: string;
scriptpubkey_asm: string;
scriptpubkey_address: string;
}
export interface Status {
confirmed: boolean;
block_height?: number;
block_hash?: string;
block_time?: number;
}
export interface Block {
id: string;
height: number;
version: number;
timestamp: number;
bits: number;
nounce: number;
difficulty: number;
merkle_root: string;
tx_count: number;
size: number;
weight: number;
previousblockhash: string;
// Custom properties
medianFee?: number;
feeRange?: number[];
reward?: number;
coinbaseTx?: TransactionMinerInfo;
matchRate: number;
stage: number;
}
export interface Address {
address: string;
chain_stats: ChainStats;
mempool_stats: MempoolStats;
}
export interface ChainStats {
funded_txo_count: number;
funded_txo_sum: number;
spent_txo_count: number;
spent_txo_sum: number;
tx_count: number;
}
export interface MempoolStats {
funded_txo_count: number;
funded_txo_sum: number;
spent_txo_count: number;
spent_txo_sum: number;
tx_count: number;
}
export interface Statistic {
id?: number;
added: string;
unconfirmed_transactions: number;
tx_per_second: number;
vbytes_per_second: number;
total_fee: number;
mempool_byte_weight: number;
fee_data: string;
vsize_1: number;
vsize_2: number;
vsize_3: number;
vsize_4: number;
vsize_5: number;
vsize_6: number;
vsize_8: number;
vsize_10: number;
vsize_12: number;
vsize_15: number;
vsize_20: number;
vsize_30: number;
vsize_40: number;
vsize_50: number;
vsize_60: number;
vsize_70: number;
vsize_80: number;
vsize_90: number;
vsize_100: number;
vsize_125: number;
vsize_150: number;
vsize_175: number;
vsize_200: number;
vsize_250: number;
vsize_300: number;
vsize_350: number;
vsize_400: number;
vsize_500: number;
vsize_600: number;
vsize_700: number;
vsize_800: number;
vsize_900: number;
vsize_1000: number;
vsize_1200: number;
vsize_1400: number;
vsize_1600: number;
vsize_1800: number;
vsize_2000: number;
}
export interface OptimizedStatistic {
id: number;
added: string;
unconfirmed_transactions: number;
tx_per_second: number;
vbytes_per_second: number;
total_fee: number;
mempool_byte_weight: number;
vsizes: number[];
}
export interface Outspend {
spent: boolean;
txid: string;
vin: number;
status: Status;
}
export interface WebsocketResponse {
action: string;
data: string[];
'track-tx': string;
'track-address': string;
'watch-mempool': boolean;
}
export interface VbytesPerSecond {
unixTime: number;
vSize: number;
}
export interface BisqBlocks {
chainHeight: number;
blocks: BisqBlock[];
}
export interface BisqBlock {
height: number;
time: number;
hash: string;
previousBlockHash: string;
txs: BisqTransaction[];
}
export interface BisqTransaction {
txVersion: string;
id: string;
blockHeight: number;
blockHash: string;
time: number;
inputs: BisqInput[];
outputs: BisqOutput[];
txType: string;
txTypeDisplayString: string;
burntFee: number;
invalidatedBsq: number;
unlockBlockHeight: number;
}
export interface BisqStats {
minted: number;
burnt: number;
addresses: number;
unspent_txos: number;
spent_txos: number;
}
interface BisqInput {
spendingTxOutputIndex: number;
spendingTxId: string;
bsqAmount: number;
isVerified: boolean;
address: string;
time: number;
}
interface BisqOutput {
txVersion: string;
txId: string;
index: number;
bsqAmount: number;
btcAmount: number;
height: number;
isVerified: boolean;
burntFee: number;
invalidatedBsq: number;
address: string;
scriptPubKey: BisqScriptPubKey;
time: any;
txType: string;
txTypeDisplayString: string;
txOutputType: string;
txOutputTypeDisplayString: string;
lockTime: number;
isUnspent: boolean;
spentInfo: SpentInfo;
opReturn?: string;
}
interface BisqScriptPubKey {
addresses: string[];
asm: string;
hex: string;
reqSigs: number;
type: string;
}
interface SpentInfo {
height: number;
inputIndex: number;
txId: string;
}
export interface BisqTrade {
direction: string;
price: string;
amount: string;
volume: string;
payment_method: string;
trade_id: string;
trade_date: number;
}

149
backend/src/logger.ts Normal file
View File

@@ -0,0 +1,149 @@
import config from './config';
import * as dgram from 'dgram';
class Logger {
static priorities = {
emerg: 0,
alert: 1,
crit: 2,
err: 3,
warn: 4,
notice: 5,
info: 6,
debug: 7
};
static facilities = {
kern: 0,
user: 1,
mail: 2,
daemon: 3,
auth: 4,
syslog: 5,
lpr: 6,
news: 7,
uucp: 8,
local0: 16,
local1: 17,
local2: 18,
local3: 19,
local4: 20,
local5: 21,
local6: 22,
local7: 23
};
// @ts-ignore
public emerg: ((msg: string) => void);
// @ts-ignore
public alert: ((msg: string) => void);
// @ts-ignore
public crit: ((msg: string) => void);
// @ts-ignore
public err: ((msg: string) => void);
// @ts-ignore
public warn: ((msg: string) => void);
// @ts-ignore
public notice: ((msg: string) => void);
// @ts-ignore
public info: ((msg: string) => void);
// @ts-ignore
public debug: ((msg: string) => void);
private name = 'mempool';
private fac: any;
private loghost: string;
private logport: number;
private client: dgram.Socket;
private network: string;
constructor(fac) {
let prio;
this.fac = fac != null ? fac : Logger.facilities.local0;
this.loghost = '127.0.0.1';
this.logport = 514;
for (prio in Logger.priorities) {
if (true) {
this.addprio(prio);
}
}
this.client = dgram.createSocket('udp4');
this.network = this.getNetwork();
}
private addprio(prio): void {
this[prio] = (function(_this) {
return function(msg) {
return _this.msg(prio, msg);
};
})(this);
}
private getNetwork(): string {
if (config.BISQ_BLOCKS.ENABLED) {
return 'bisq';
}
if (config.MEMPOOL.NETWORK && config.MEMPOOL.NETWORK !== 'mainnet') {
return config.MEMPOOL.NETWORK;
}
return '';
}
private msg(priority, msg) {
let consolemsg, prionum, syslogmsg;
if (typeof msg === 'string' && msg.length > 0) {
while (msg[msg.length - 1].charCodeAt(0) === 10) {
msg = msg.slice(0, msg.length - 1);
}
}
const network = this.network ? ' <' + this.network + '>' : '';
prionum = Logger.priorities[priority] || Logger.priorities.info;
syslogmsg = `<${(this.fac * 8 + prionum)}> ${this.name}[${process.pid}]: ${priority.toUpperCase()}${network} ${msg}`;
consolemsg = `${this.ts()} [${process.pid}] ${priority.toUpperCase()}:${network} ${msg}`;
this.syslog(syslogmsg);
if (priority === 'warning') {
priority = 'warn';
}
if (priority === 'debug') {
priority = 'info';
}
if (priority === 'err') {
priority = 'error';
}
return (console[priority] || console.error)(consolemsg);
}
private syslog(msg) {
let msgbuf;
msgbuf = Buffer.from(msg);
this.client.send(msgbuf, 0, msgbuf.length, this.logport, this.loghost, function(err, bytes) {
if (err) {
console.log(err);
}
});
}
private leadZero(n: number): number | string {
if (n < 10) {
return '0' + n;
}
return n;
}
private ts() {
let day, dt, hours, minutes, month, months, seconds;
dt = new Date();
hours = this.leadZero(dt.getHours());
minutes = this.leadZero(dt.getMinutes());
seconds = this.leadZero(dt.getSeconds());
month = dt.getMonth();
day = dt.getDate();
if (day < 10) {
day = ' ' + day;
}
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return months[month] + ' ' + day + ' ' + hours + ':' + minutes + ':' + seconds;
}
}
export default new Logger(Logger.facilities.local7);

View File

@@ -0,0 +1,140 @@
import { IEsploraApi } from './api/bitcoin/esplora-api.interface';
export interface MempoolBlock {
blockSize: number;
blockVSize: number;
nTx: number;
medianFee: number;
totalFees: number;
feeRange: number[];
}
export interface MempoolBlockWithTransactions extends MempoolBlock {
transactionIds: string[];
}
interface VinStrippedToScriptsig {
scriptsig: string;
}
interface VoutStrippedToScriptPubkey {
scriptpubkey_address: string | undefined;
value: number;
}
export interface TransactionExtended extends IEsploraApi.Transaction {
vsize: number;
feePerVsize: number;
firstSeen?: number;
}
export interface TransactionStripped {
txid: string;
fee: number;
vsize: number;
value: number;
}
export interface BlockExtended extends IEsploraApi.Block {
medianFee?: number;
feeRange?: number[];
reward?: number;
coinbaseTx?: TransactionMinerInfo;
matchRate?: number;
}
export interface TransactionMinerInfo {
vin: VinStrippedToScriptsig[];
vout: VoutStrippedToScriptPubkey[];
}
export interface MempoolStats {
funded_txo_count: number;
funded_txo_sum: number;
spent_txo_count: number;
spent_txo_sum: number;
tx_count: number;
}
export interface Statistic {
id?: number;
added: string;
unconfirmed_transactions: number;
tx_per_second: number;
vbytes_per_second: number;
total_fee: number;
mempool_byte_weight: number;
fee_data: string;
vsize_1: number;
vsize_2: number;
vsize_3: number;
vsize_4: number;
vsize_5: number;
vsize_6: number;
vsize_8: number;
vsize_10: number;
vsize_12: number;
vsize_15: number;
vsize_20: number;
vsize_30: number;
vsize_40: number;
vsize_50: number;
vsize_60: number;
vsize_70: number;
vsize_80: number;
vsize_90: number;
vsize_100: number;
vsize_125: number;
vsize_150: number;
vsize_175: number;
vsize_200: number;
vsize_250: number;
vsize_300: number;
vsize_350: number;
vsize_400: number;
vsize_500: number;
vsize_600: number;
vsize_700: number;
vsize_800: number;
vsize_900: number;
vsize_1000: number;
vsize_1200: number;
vsize_1400: number;
vsize_1600: number;
vsize_1800: number;
vsize_2000: number;
}
export interface OptimizedStatistic {
id: number;
added: string;
unconfirmed_transactions: number;
tx_per_second: number;
vbytes_per_second: number;
total_fee: number;
mempool_byte_weight: number;
vsizes: number[];
}
export interface WebsocketResponse {
action: string;
data: string[];
'track-tx': string;
'track-address': string;
'watch-mempool': boolean;
}
export interface VbytesPerSecond {
unixTime: number;
vSize: number;
}
export interface RequiredSpec { [name: string]: RequiredParams; }
interface RequiredParams {
required: boolean;
types: ('@string' | '@number' | '@boolean' | string)[];
}
export interface ILoadingIndicators { [name: string]: number; }
export interface IConversionRates { [currency: string]: number; }

View File

@@ -1,28 +1,25 @@
import config from './config';
import { Request, Response } from 'express';
import statistics from './api/statistics';
import feeApi from './api/fee-api';
import backendInfo from './api/backend-info';
import mempoolBlocks from './api/mempool-blocks';
import mempool from './api/mempool';
import bisq from './api/bisq';
import bisq from './api/bisq/bisq';
import websocketHandler from './api/websocket-handler';
import bisqMarket from './api/bisq/markets-api';
import { RequiredSpec, TransactionExtended } from './mempool.interfaces';
import { MarketsApiError } from './api/bisq/interfaces';
import { IEsploraApi } from './api/bitcoin/esplora-api.interface';
import logger from './logger';
import bitcoinApi from './api/bitcoin/bitcoin-api-factory';
import transactionUtils from './api/transaction-utils';
import blocks from './api/blocks';
import loadingIndicators from './api/loading-indicators';
import { Common } from './api/common';
class Routes {
private cache = {};
constructor() {
this.createCache();
setInterval(this.createCache.bind(this), 600000);
}
private async createCache() {
this.cache['24h'] = await statistics.$list24H();
this.cache['1w'] = await statistics.$list1W();
this.cache['1m'] = await statistics.$list1M();
this.cache['3m'] = await statistics.$list3M();
this.cache['6m'] = await statistics.$list6M();
this.cache['1y'] = await statistics.$list1Y();
console.log('Statistics cache created');
}
constructor() {}
public async get2HStatistics(req: Request, res: Response) {
const result = await statistics.$list2H();
@@ -30,30 +27,44 @@ class Routes {
}
public get24HStatistics(req: Request, res: Response) {
res.json(this.cache['24h']);
res.json(statistics.getCache()['24h']);
}
public get1WHStatistics(req: Request, res: Response) {
res.json(this.cache['1w']);
res.json(statistics.getCache()['1w']);
}
public get1MStatistics(req: Request, res: Response) {
res.json(this.cache['1m']);
res.json(statistics.getCache()['1m']);
}
public get3MStatistics(req: Request, res: Response) {
res.json(this.cache['3m']);
res.json(statistics.getCache()['3m']);
}
public get6MStatistics(req: Request, res: Response) {
res.json(this.cache['6m']);
res.json(statistics.getCache()['6m']);
}
public get1YStatistics(req: Request, res: Response) {
res.json(this.cache['1y']);
res.json(statistics.getCache()['1y']);
}
public getInitData(req: Request, res: Response) {
try {
const result = websocketHandler.getInitData();
res.json(result);
} catch (e) {
res.status(500).send(e.message);
}
}
public async getRecommendedFees(req: Request, res: Response) {
if (!mempool.isInSync()) {
res.statusCode = 503;
res.send('Service Unavailable');
return;
}
const result = feeApi.getRecommendedFee();
res.json(result);
}
@@ -153,6 +164,474 @@ class Routes {
res.status(404).send('Bisq address not found');
}
}
public getBisqMarketCurrencies(req: Request, res: Response) {
const constraints: RequiredSpec = {
'type': {
required: false,
types: ['crypto', 'fiat', 'all']
},
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = bisqMarket.getCurrencies(p.type);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketCurrencies error'));
}
}
public getBisqMarketDepth(req: Request, res: Response) {
const constraints: RequiredSpec = {
'market': {
required: true,
types: ['@string']
},
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = bisqMarket.getDepth(p.market);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketDepth error'));
}
}
public getBisqMarketMarkets(req: Request, res: Response) {
const result = bisqMarket.getMarkets();
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketMarkets error'));
}
}
public getBisqMarketTrades(req: Request, res: Response) {
const constraints: RequiredSpec = {
'market': {
required: true,
types: ['@string']
},
'timestamp_from': {
required: false,
types: ['@number']
},
'timestamp_to': {
required: false,
types: ['@number']
},
'trade_id_to': {
required: false,
types: ['@string']
},
'trade_id_from': {
required: false,
types: ['@string']
},
'direction': {
required: false,
types: ['buy', 'sell']
},
'limit': {
required: false,
types: ['@number']
},
'sort': {
required: false,
types: ['asc', 'desc']
}
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = bisqMarket.getTrades(p.market, p.timestamp_from,
p.timestamp_to, p.trade_id_from, p.trade_id_to, p.direction, p.limit, p.sort);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketTrades error'));
}
}
public getBisqMarketOffers(req: Request, res: Response) {
const constraints: RequiredSpec = {
'market': {
required: true,
types: ['@string']
},
'direction': {
required: false,
types: ['buy', 'sell']
},
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = bisqMarket.getOffers(p.market, p.direction);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketOffers error'));
}
}
public getBisqMarketVolumes(req: Request, res: Response) {
const constraints: RequiredSpec = {
'market': {
required: false,
types: ['@string']
},
'interval': {
required: false,
types: ['minute', 'half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto']
},
'timestamp_from': {
required: false,
types: ['@number']
},
'timestamp_to': {
required: false,
types: ['@number']
},
'milliseconds': {
required: false,
types: ['@boolean']
},
'timestamp': {
required: false,
types: ['no', 'yes']
},
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = bisqMarket.getVolumes(p.market, p.timestamp_from, p.timestamp_to, p.interval, p.milliseconds, p.timestamp);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketVolumes error'));
}
}
public getBisqMarketHloc(req: Request, res: Response) {
const constraints: RequiredSpec = {
'market': {
required: true,
types: ['@string']
},
'interval': {
required: false,
types: ['minute', 'half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto']
},
'timestamp_from': {
required: false,
types: ['@number']
},
'timestamp_to': {
required: false,
types: ['@number']
},
'milliseconds': {
required: false,
types: ['@boolean']
},
'timestamp': {
required: false,
types: ['no', 'yes']
},
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = bisqMarket.getHloc(p.market, p.interval, p.timestamp_from, p.timestamp_to, p.milliseconds, p.timestamp);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketHloc error'));
}
}
public getBisqMarketTicker(req: Request, res: Response) {
const constraints: RequiredSpec = {
'market': {
required: false,
types: ['@string']
},
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = bisqMarket.getTicker(p.market);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketTicker error'));
}
}
private parseRequestParameters(requestParams: object, params: RequiredSpec): { [name: string]: any; } {
const final = {};
for (const i in params) {
if (params.hasOwnProperty(i)) {
if (params[i].required && requestParams[i] === undefined) {
return { error: i + ' parameter missing'};
}
if (typeof requestParams[i] === 'string') {
const str = (requestParams[i] || '').toString().toLowerCase();
if (params[i].types.indexOf('@number') > -1) {
const number = parseInt((str).toString(), 10);
final[i] = number;
} else if (params[i].types.indexOf('@string') > -1) {
final[i] = str;
} else if (params[i].types.indexOf('@boolean') > -1) {
final[i] = str === 'true' || str === 'yes';
} else if (params[i].types.indexOf(str) > -1) {
final[i] = str;
} else {
return { error: i + ' parameter invalid'};
}
} else if (typeof requestParams[i] === 'number') {
final[i] = requestParams[i];
}
}
}
return final;
}
private getBisqMarketErrorResponse(message: string): MarketsApiError {
return {
'success': 0,
'error': message
};
}
public async getTransaction(req: Request, res: Response) {
try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
res.json(transaction);
} catch (e) {
let statusCode = 500;
if (e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
}
res.status(statusCode).send(e.message || e);
}
}
public async getTransactionStatus(req: Request, res: Response) {
try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
res.json(transaction.status);
} catch (e) {
let statusCode = 500;
if (e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
}
res.status(statusCode).send(e.message || e);
}
}
public async getBlock(req: Request, res: Response) {
try {
const result = await bitcoinApi.$getBlock(req.params.hash);
res.json(result);
} catch (e) {
res.status(500).send(e.message || e);
}
}
public async getBlocks(req: Request, res: Response) {
try {
loadingIndicators.setProgress('blocks', 0);
const returnBlocks: IEsploraApi.Block[] = [];
const fromHeight = parseInt(req.params.height, 10) || blocks.getCurrentBlockHeight();
// Check if block height exist in local cache to skip the hash lookup
const blockByHeight = blocks.getBlocks().find((b) => b.height === fromHeight);
let startFromHash: string | null = null;
if (blockByHeight) {
startFromHash = blockByHeight.id;
} else {
startFromHash = await bitcoinApi.$getBlockHash(fromHeight);
}
let nextHash = startFromHash;
for (let i = 0; i < 10; i++) {
const localBlock = blocks.getBlocks().find((b) => b.id === nextHash);
if (localBlock) {
returnBlocks.push(localBlock);
nextHash = localBlock.previousblockhash;
} else {
const block = await bitcoinApi.$getBlock(nextHash);
returnBlocks.push(block);
nextHash = block.previousblockhash;
}
loadingIndicators.setProgress('blocks', i / 10 * 100);
}
res.json(returnBlocks);
} catch (e) {
loadingIndicators.setProgress('blocks', 100);
res.status(500).send(e.message || e);
}
}
public async getBlockTransactions(req: Request, res: Response) {
try {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
const txIds = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
const transactions: TransactionExtended[] = [];
const startingIndex = Math.max(0, parseInt(req.params.index || '0', 10));
const endIndex = Math.min(startingIndex + 10, txIds.length);
for (let i = startingIndex; i < endIndex; i++) {
try {
const transaction = await transactionUtils.$getTransactionExtended(txIds[i], true);
transactions.push(transaction);
loadingIndicators.setProgress('blocktxs-' + req.params.hash, (i + 1) / endIndex * 100);
} catch (e) {
logger.debug('getBlockTransactions error: ' + e.message || e);
}
}
res.json(transactions);
} catch (e) {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
res.status(500).send(e.message || e);
}
}
public async getBlockHeight(req: Request, res: Response) {
try {
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
res.send(blockHash);
} catch (e) {
res.status(500).send(e.message || e);
}
}
public async getAddress(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
try {
const addressData = await bitcoinApi.$getAddress(req.params.address);
res.json(addressData);
} catch (e) {
if (e.message && e.message.indexOf('exceeds') > 0) {
return res.status(413).send(e.message);
}
res.status(500).send(e.message || e);
}
}
public async getAddressTransactions(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
try {
const transactions = await bitcoinApi.$getAddressTransactions(req.params.address, req.params.txId);
res.json(transactions);
} catch (e) {
if (e.message && e.message.indexOf('exceeds') > 0) {
return res.status(413).send(e.message);
}
res.status(500).send(e.message || e);
}
}
public async getAdressTxChain(req: Request, res: Response) {
res.status(501).send('Not implemented');
}
public async getAddressPrefix(req: Request, res: Response) {
try {
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
res.send(blockHash);
} catch (e) {
res.status(500).send(e.message || e);
}
}
public async getRecentMempoolTransactions(req: Request, res: Response) {
const latestTransactions = Object.entries(mempool.getMempool())
.sort((a, b) => (b[1].firstSeen || 0) - (a[1].firstSeen || 0))
.slice(0, 10).map((tx) => Common.stripTransaction(tx[1]));
res.json(latestTransactions);
}
public async getMempool(req: Request, res: Response) {
res.status(501).send('Not implemented');
}
public async getMempoolTxIds(req: Request, res: Response) {
try {
const rawMempool = await bitcoinApi.$getRawMempool();
res.send(rawMempool);
} catch (e) {
res.status(500).send(e.message || e);
}
}
public async getBlockTipHeight(req: Request, res: Response) {
try {
const result = await bitcoinApi.$getBlockHeightTip();
res.json(result);
} catch (e) {
res.status(500).send(e.message || e);
}
}
public async getTxIdsForBlock(req: Request, res: Response) {
try {
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
res.json(result);
} catch (e) {
res.status(500).send(e.message || e);
}
}
public getTransactionOutspends(req: Request, res: Response) {
res.status(501).send('Not implemented');
}
}
export default new Routes();

View File

@@ -1,7 +1,8 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2015",
"target": "esnext",
"lib": ["es2019"],
"strict": true,
"noImplicitAny": false,
"sourceMap": false,

View File

@@ -12,7 +12,7 @@
"severity": "warn"
},
"eofline": true,
"forin": true,
"forin": false,
"import-blacklist": [
true,
"rxjs",

File diff suppressed because it is too large Load Diff

99
docker/README.md Normal file
View File

@@ -0,0 +1,99 @@
# Docker
## Initialization
In an empty dir create 2 sub-dirs
```bash
mkdir -p data mysql/data mysql/db-scripts
```
In the `mysql/db-scripts` sub-dir add the `mariadb-structure.sql` file from the mempool repo
Your dir should now look like that:
```bash
$ls -R
.:
data mysql
./data:
./mysql:
data db-scripts
./mysql/data:
./mysql/db-scripts:
mariadb-structure.sql
```
In the main dir add the following `docker-compose.yml`
```bash
version: "3.7"
services:
web:
image: mempool/frontend:latest
user: "1000:1000"
restart: on-failure
stop_grace_period: 1m
command: "./wait-for db:3306 --timeout=720 -- nginx -g 'daemon off;'"
ports:
- 80:8080
environment:
FRONTEND_HTTP_PORT: "8080"
BACKEND_MAINNET_HTTP_HOST: "api"
api:
image: mempool/backend:latest
user: "1000:1000"
restart: on-failure
stop_grace_period: 1m
command: "./wait-for-it.sh db:3306 --timeout=720 --strict -- ./start.sh"
volumes:
- ./data:/backend/cache
environment:
RPC_HOST: "127.0.0.1"
RPC_PORT: "8332"
RPC_USER: "mempool"
RPC_PASS: "mempool"
ELECTRS_HOST: "127.0.0.1"
ELECTRS_PORT: "50002"
MYSQL_HOST: "db"
MYSQL_PORT: "3306"
MYSQL_DATABASE: "mempool"
MYSQL_USER: "mempool"
MYSQL_PASS: "mempool"
BACKEND_MAINNET_HTTP_PORT: "8999"
CACHE_DIR: "/backend/cache/"
db:
image: mariadb:10.5.8
user: "1000:1000"
restart: on-failure
stop_grace_period: 1m
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/db-scripts:/docker-entrypoint-initdb.d
environment:
MYSQL_DATABASE: "mempool"
MYSQL_USER: "mempool"
MYSQL_PASSWORD: "mempool"
MYSQL_ROOT_PASSWORD: "admin"
```
You can update all the environment variables inside the API container, especially the RPC and ELECTRS ones
## Run it
To run our docker-compose use the following cmd:
```bash
docker-compose up
```
If everything went okay you should see the beautiful mempool :grin:
If you get stuck on "loading blocks", this means the websocket can't connect.
Check your nginx proxy setup, firewalls, etc. and open an issue if you need help.

27
docker/backend/Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
FROM node:12-buster-slim AS builder
WORKDIR /build
COPY . .
RUN apt-get update
RUN apt-get install -y build-essential python3 pkg-config
RUN npm ci --production
RUN npm i typescript
RUN npm run build
FROM node:12-buster-slim
WORKDIR /backend
COPY --from=builder /build/ .
RUN chmod +x /backend/start.sh
RUN chmod +x /backend/wait-for-it.sh
RUN chown -R 1000:1000 /backend && chmod -R 755 /backend
USER 1000
EXPOSE 8999
CMD ["/backend/start.sh"]

View File

@@ -0,0 +1,38 @@
{
"MEMPOOL": {
"NETWORK": "mainnet",
"BACKEND": "electrum",
"HTTP_PORT": __MEMPOOL_BACKEND_MAINNET_HTTP_PORT__,
"SPAWN_CLUSTER_PROCS": 0,
"API_URL_PREFIX": "/api/v1/",
"POLL_RATE_MS": 2000,
"CACHE_DIR": "__MEMPOOL_BACKEND_MAINNET_CACHE_DIR__"
},
"CORE_RPC": {
"HOST": "__BITCOIN_MAINNET_RPC_HOST__",
"PORT": __BITCOIN_MAINNET_RPC_PORT__,
"USERNAME": "__BITCOIN_MAINNET_RPC_USER__",
"PASSWORD": "__BITCOIN_MAINNET_RPC_PASS__"
},
"ELECTRUM": {
"HOST": "__ELECTRS_MAINNET_HTTP_HOST__",
"PORT": __ELECTRS_MAINNET_HTTP_PORT__,
"TLS_ENABLED": false,
"TX_LOOKUPS": true
},
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:3000"
},
"DATABASE": {
"ENABLED": true,
"HOST": "__MYSQL_HOST__",
"PORT": __MYSQL_PORT__,
"DATABASE": "__MYSQL_DATABASE__",
"USERNAME": "__MYSQL_USERNAME__",
"PASSWORD": "__MYSQL_PASSWORD__"
},
"STATISTICS": {
"ENABLED": true,
"TX_PER_SECOND_SAMPLE_PERIOD": 150
}
}

37
docker/backend/start.sh Normal file
View File

@@ -0,0 +1,37 @@
#!/bin/sh
#MEMPOOL
__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__=${BACKEND_MAINNET_HTTP_PORT:=8999}
__MEMPOOL_BACKEND_MAINNET_CACHE_DIR__=${CACHE_DIR:=./}
# BITCOIN
__BITCOIN_MAINNET_RPC_HOST__=${RPC_HOST:=127.0.0.1}
__BITCOIN_MAINNET_RPC_PORT__=${RPC_PORT:=8332}
__BITCOIN_MAINNET_RPC_USER__=${RPC_USER:=mempool}
__BITCOIN_MAINNET_RPC_PASS__=${RPC_PASS:=mempool}
# ELECTRUM
__ELECTRS_MAINNET_HTTP_HOST__=${ELECTRS_HOST:=127.0.0.1}
__ELECTRS_MAINNET_HTTP_PORT__=${ELECTRS_PORT:=50002}
# MYSQL
__MYSQL_HOST__=${MYSQL_HOST:=127.0.0.1}
__MYSQL_PORT__=${MYSQL_PORT:=3306}
__MYSQL_DATABASE__=${MYSQL_DATABASE:=mempool}
__MYSQL_USERNAME__=${MYSQL_USER:=mempool}
__MYSQL_PASSWORD__=${MYSQL_PASS:=mempool}
mkdir -p "${__MEMPOOL_BACKEND_MAINNET_CACHE_DIR__}"
sed -i "s/__BITCOIN_MAINNET_RPC_HOST__/${__BITCOIN_MAINNET_RPC_HOST__}/g" mempool-config.json
sed -i "s/__BITCOIN_MAINNET_RPC_PORT__/${__BITCOIN_MAINNET_RPC_PORT__}/g" mempool-config.json
sed -i "s/__BITCOIN_MAINNET_RPC_USER__/${__BITCOIN_MAINNET_RPC_USER__}/g" mempool-config.json
sed -i "s/__BITCOIN_MAINNET_RPC_PASS__/${__BITCOIN_MAINNET_RPC_PASS__}/g" mempool-config.json
sed -i "s/__ELECTRS_MAINNET_HTTP_HOST__/${__ELECTRS_MAINNET_HTTP_HOST__}/g" mempool-config.json
sed -i "s/__ELECTRS_MAINNET_HTTP_PORT__/${__ELECTRS_MAINNET_HTTP_PORT__}/g" mempool-config.json
sed -i "s/__MYSQL_HOST__/${__MYSQL_HOST__}/g" mempool-config.json
sed -i "s/__MYSQL_PORT__/${__MYSQL_PORT__}/g" mempool-config.json
sed -i "s/__MYSQL_DATABASE__/${__MYSQL_DATABASE__}/g" mempool-config.json
sed -i "s/__MYSQL_USERNAME__/${__MYSQL_USERNAME__}/g" mempool-config.json
sed -i "s/__MYSQL_PASSWORD__/${__MYSQL_PASSWORD__}/g" mempool-config.json
sed -i "s!__MEMPOOL_BACKEND_MAINNET_CACHE_DIR__!${__MEMPOOL_BACKEND_MAINNET_CACHE_DIR__}!g" mempool-config.json
sed -i "s/__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/${__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__}/g" mempool-config.json
node /backend/dist/index.js

View File

@@ -0,0 +1,182 @@
#!/usr/bin/env bash
# Use this script to test if a given TCP host/port are available
WAITFORIT_cmdname=${0##*/}
echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
usage()
{
cat << USAGE >&2
Usage:
$WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
-h HOST | --host=HOST Host or IP under test
-p PORT | --port=PORT TCP port under test
Alternatively, you specify the host and port as host:port
-s | --strict Only execute subcommand if the test succeeds
-q | --quiet Don't output any status messages
-t TIMEOUT | --timeout=TIMEOUT
Timeout in seconds, zero for no timeout
-- COMMAND ARGS Execute command with args after the test finishes
USAGE
exit 1
}
wait_for()
{
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
else
echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
fi
WAITFORIT_start_ts=$(date +%s)
while :
do
if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
nc -z $WAITFORIT_HOST $WAITFORIT_PORT
WAITFORIT_result=$?
else
(echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
WAITFORIT_result=$?
fi
if [[ $WAITFORIT_result -eq 0 ]]; then
WAITFORIT_end_ts=$(date +%s)
echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
break
fi
sleep 1
done
return $WAITFORIT_result
}
wait_for_wrapper()
{
# In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
if [[ $WAITFORIT_QUIET -eq 1 ]]; then
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
else
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
fi
WAITFORIT_PID=$!
trap "kill -INT -$WAITFORIT_PID" INT
wait $WAITFORIT_PID
WAITFORIT_RESULT=$?
if [[ $WAITFORIT_RESULT -ne 0 ]]; then
echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
fi
return $WAITFORIT_RESULT
}
# process arguments
while [[ $# -gt 0 ]]
do
case "$1" in
*:* )
WAITFORIT_hostport=(${1//:/ })
WAITFORIT_HOST=${WAITFORIT_hostport[0]}
WAITFORIT_PORT=${WAITFORIT_hostport[1]}
shift 1
;;
--child)
WAITFORIT_CHILD=1
shift 1
;;
-q | --quiet)
WAITFORIT_QUIET=1
shift 1
;;
-s | --strict)
WAITFORIT_STRICT=1
shift 1
;;
-h)
WAITFORIT_HOST="$2"
if [[ $WAITFORIT_HOST == "" ]]; then break; fi
shift 2
;;
--host=*)
WAITFORIT_HOST="${1#*=}"
shift 1
;;
-p)
WAITFORIT_PORT="$2"
if [[ $WAITFORIT_PORT == "" ]]; then break; fi
shift 2
;;
--port=*)
WAITFORIT_PORT="${1#*=}"
shift 1
;;
-t)
WAITFORIT_TIMEOUT="$2"
if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
shift 2
;;
--timeout=*)
WAITFORIT_TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
WAITFORIT_CLI=("$@")
break
;;
--help)
usage
;;
*)
echoerr "Unknown argument: $1"
usage
;;
esac
done
if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
echoerr "Error: you need to provide a host and port to test."
usage
fi
WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
# Check to see if timeout is from busybox?
WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
WAITFORIT_BUSYTIMEFLAG=""
if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
WAITFORIT_ISBUSY=1
# Check if busybox timeout uses -t flag
# (recent Alpine versions don't support -t anymore)
if timeout &>/dev/stdout | grep -q -e '-t '; then
WAITFORIT_BUSYTIMEFLAG="-t"
fi
else
WAITFORIT_ISBUSY=0
fi
if [[ $WAITFORIT_CHILD -gt 0 ]]; then
wait_for
WAITFORIT_RESULT=$?
exit $WAITFORIT_RESULT
else
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
wait_for_wrapper
WAITFORIT_RESULT=$?
else
wait_for
WAITFORIT_RESULT=$?
fi
fi
if [[ $WAITFORIT_CLI != "" ]]; then
if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
exit $WAITFORIT_RESULT
fi
exec "${WAITFORIT_CLI[@]}"
else
exit $WAITFORIT_RESULT
fi

View File

@@ -0,0 +1,34 @@
FROM node:12-buster-slim AS builder
WORKDIR /build
COPY . .
RUN apt-get update
RUN apt-get install -y build-essential rsync
RUN npm i
RUN npm run build
FROM nginx:1.17.8-alpine
WORKDIR /patch
COPY --from=builder /build/entrypoint.sh .
COPY --from=builder /build/wait-for .
COPY --from=builder /build/dist/mempool /var/www/mempool
COPY --from=builder /build/nginx.conf /etc/nginx/
COPY --from=builder /build/nginx-mempool.conf /etc/nginx/conf.d/
RUN chmod +x /patch/entrypoint.sh
RUN chmod +x /patch/wait-for
RUN chown -R 1000:1000 /patch && chmod -R 755 /patch && \
chown -R 1000:1000 /var/cache/nginx && \
chown -R 1000:1000 /var/log/nginx && \
chown -R 1000:1000 /etc/nginx/nginx.conf && \
chown -R 1000:1000 /etc/nginx/conf.d
RUN touch /var/run/nginx.pid && \
chown -R 1000:1000 /var/run/nginx.pid
USER 1000
ENTRYPOINT ["/patch/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,13 @@
#!/bin/sh
__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__=${BACKEND_MAINNET_HTTP_HOST:=127.0.0.1}
__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__=${BACKEND_MAINNET_HTTP_PORT:=8999}
__MEMPOOL_FRONTEND_HTTP_PORT__=${FRONTEND_HTTP_PORT:=8080}
sed -i "s/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__/${__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__}/g" /etc/nginx/conf.d/nginx-mempool.conf
sed -i "s/__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/${__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__}/g" /etc/nginx/conf.d/nginx-mempool.conf
cp /etc/nginx/nginx.conf /patch/nginx.conf
sed -i "s/__MEMPOOL_FRONTEND_HTTP_PORT__/${__MEMPOOL_FRONTEND_HTTP_PORT__}/g" /patch/nginx.conf
cat /patch/nginx.conf > /etc/nginx/nginx.conf
exec "$@"

84
docker/frontend/wait-for Normal file
View File

@@ -0,0 +1,84 @@
#!/bin/sh
TIMEOUT=15
QUIET=0
echoerr() {
if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi
}
usage() {
exitcode="$1"
cat << USAGE >&2
Usage:
$cmdname host:port [-t timeout] [-- command args]
-q | --quiet Do not output any status messages
-t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout
-- COMMAND ARGS Execute command with args after the test finishes
USAGE
exit "$exitcode"
}
wait_for() {
if ! command -v nc >/dev/null; then
echoerr 'nc command is missing!'
exit 1
fi
for i in `seq $TIMEOUT` ; do
nc -z "$HOST" "$PORT" > /dev/null 2>&1
result=$?
if [ $result -eq 0 ] ; then
if [ $# -gt 0 ] ; then
exec "$@"
fi
exit 0
fi
sleep 1
done
echo "Operation timed out" >&2
exit 1
}
while [ $# -gt 0 ]
do
case "$1" in
*:* )
HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
shift 1
;;
-q | --quiet)
QUIET=1
shift 1
;;
-t)
TIMEOUT="$2"
if [ "$TIMEOUT" = "" ]; then break; fi
shift 2
;;
--timeout=*)
TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
break
;;
--help)
usage 0
;;
*)
echoerr "Unknown argument: $1"
usage 1
;;
esac
done
if [ "$HOST" = "" -o "$PORT" = "" ]; then
echoerr "Error: you need to provide a host and port to test."
usage 2
fi
wait_for "$@"

18
docker/init.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/sh
#backend
gitMaster="\.\.\/\.git\/refs\/heads\/master"
git ls-remote https://github.com/mempool/mempool.git $1 | awk '{ print $1}' > ./backend/master
cp ./docker/backend/* ./backend/
sed -i "s/${gitMaster}/master/g" ./backend/src/api/backend-info.ts
#frontend
localhostIP="127.0.0.1"
cp ./docker/frontend/* ./frontend
cp ./nginx.conf ./frontend/
cp ./nginx-mempool.conf ./frontend/
sed -i "s/${localhostIP}:80/0.0.0.0:__MEMPOOL_FRONTEND_HTTP_PORT__/g" ./frontend/nginx.conf
sed -i "s/${localhostIP}/0.0.0.0/g" ./frontend/nginx.conf
sed -i "s/user nobody;//g" ./frontend/nginx.conf
sed -i "s!/etc/nginx/nginx-mempool.conf!/etc/nginx/conf.d/nginx-mempool.conf!g" ./frontend/nginx.conf
sed -i "s/${localhostIP}:8999/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/g" ./frontend/nginx-mempool.conf

View File

@@ -1,10 +0,0 @@
#!/bin/sh
mysqld_safe&
sleep 5
nginx
cd /mempool.space/backend
rm -f mempool-config.json
rm -f cache.json
touch cache.json
jq -n env > mempool-config.json
node dist/index.js

2
frontend/.gitignore vendored
View File

@@ -4,6 +4,8 @@
/dist
/tmp
/out-tsc
server.run.js
# Only exists if Bazel was run
/bazel-out

7
frontend/.tx/config Normal file
View File

@@ -0,0 +1,7 @@
[main]
host = https://www.transifex.com
[mempool.frontend-src-locale-messages-xlf--master]
file_filter = frontend/src/locale/messages.<lang>.xlf
source_lang = en-US
type = XLIFF

View File

@@ -1,27 +1,31 @@
# Mempool Space
# mempool-frontend
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.1.2.
## Transifex Project
## Development server
The mempool frontend strings are localized into 20+ locales:
https://www.transifex.com/mempool/mempool/dashboard/
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Translators
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
* Arabic @baro0k
* Czech @pixelmade2
* German @Emzy
* English (default)
* Spanish @maxhodler @bisqes
* Persian @techmix
* French @Bayernatoor
* Korean @kcalvinalvinn
* Italian @HodlBits
* Georgian @wyd_idk
* Hungarian @btcdragonlord
* Dutch @m__btc
* Japanese @wiz @japananon
* Norwegian @T82771355
* Portugese @jgcastro1985
* Slovenian @thepkbadger
* Finnish @bio_bitcoin
* Swedish @softsimon_
* Turkish @stackmore
* Ukrainian @volbil
* Vietnamese @bitcoin_vietnam
* Chinese @wdljt

View File

@@ -13,11 +13,103 @@
"root": "",
"sourceRoot": "src",
"prefix": "app",
"i18n": {
"sourceLocale": {
"code":"en-US",
"baseHref":"/"
},
"locales": {
"ar": {
"translation": "src/locale/messages.ar.xlf",
"baseHref": "/ar/"
},
"cs": {
"translation": "src/locale/messages.cs.xlf",
"baseHref": "/cs/"
},
"de": {
"translation": "src/locale/messages.de.xlf",
"baseHref": "/de/"
},
"es": {
"translation": "src/locale/messages.es.xlf",
"baseHref": "/es/"
},
"fa": {
"translation": "src/locale/messages.fa.xlf",
"baseHref": "/fa/"
},
"fr": {
"translation": "src/locale/messages.fr.xlf",
"baseHref": "/fr/"
},
"ja": {
"translation": "src/locale/messages.ja.xlf",
"baseHref": "/ja/"
},
"ka": {
"translation": "src/locale/messages.ka.xlf",
"baseHref": "/ka/"
},
"ko": {
"translation": "src/locale/messages.ko.xlf",
"baseHref": "/ko/"
},
"it": {
"translation": "src/locale/messages.it.xlf",
"baseHref": "/it/"
},
"nl": {
"translation": "src/locale/messages.nl.xlf",
"baseHref": "/nl/"
},
"nb": {
"translation": "src/locale/messages.nb.xlf",
"baseHref": "/nb/"
},
"pt": {
"translation": "src/locale/messages.pt.xlf",
"baseHref": "/pt/"
},
"sl": {
"translation": "src/locale/messages.sl.xlf",
"baseHref": "/sl/"
},
"sv": {
"translation": "src/locale/messages.sv.xlf",
"baseHref": "/sv/"
},
"tr": {
"translation": "src/locale/messages.tr.xlf",
"baseHref": "/tr/"
},
"uk": {
"translation": "src/locale/messages.uk.xlf",
"baseHref": "/uk/"
},
"fi": {
"translation": "src/locale/messages.fi.xlf",
"baseHref": "/fi/"
},
"vi": {
"translation": "src/locale/messages.vi.xlf",
"baseHref": "/vi/"
},
"hu": {
"translation": "src/locale/messages.hu.xlf",
"baseHref": "/hu/"
},
"zh": {
"translation": "src/locale/messages.zh.xlf",
"baseHref": "/zh/"
}
}
},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/mempool",
"outputPath": "dist/mempool/browser",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
@@ -25,10 +117,12 @@
"aot": true,
"assets": [
"src/favicon.ico",
"src/resources"
"src/resources",
"src/robots.txt"
],
"styles": [
"src/styles.scss"
"src/styles.scss",
"node_modules/@fortawesome/fontawesome-svg-core/styles.css"
],
"scripts": [
"generated-config.js"
@@ -104,7 +198,8 @@
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
"e2e/tsconfig.json",
"tsconfig.server.json"
],
"exclude": [
"**/node_modules/**"
@@ -122,8 +217,56 @@
"devServerTarget": "mempool:serve:production"
}
}
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/mempool/server",
"main": "server.ts",
"tsConfig": "tsconfig.server.json"
},
"configurations": {
"production": {
"outputHashing": "media",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"sourceMap": false,
"localize": true,
"optimization": true
}
}
},
"serve-ssr": {
"builder": "@nguniversal/builders:ssr-dev-server",
"options": {
"browserTarget": "mempool:build",
"serverTarget": "mempool:server"
},
"configurations": {
"production": {
"browserTarget": "mempool:build:production",
"serverTarget": "mempool:server:production"
}
}
},
"prerender": {
"builder": "@nguniversal/builders:prerender",
"options": {
"browserTarget": "mempool:build:production",
"serverTarget": "mempool:server:production",
"routes": [
"/"
]
},
"configurations": {
"production": {}
}
}
}
}},
"defaultProject": "mempool"
}
}

1
frontend/frontend Symbolic link
View File

@@ -0,0 +1 @@
.

View File

@@ -25,7 +25,7 @@ for (setting in configContent) {
const code = `(function (window) {
window.__env = window.__env || {};${settings.reduce((str, obj) => `${str}
window.__env.${obj.key} = ${ typeof obj.value === 'string' ? `'${obj.value}'` : obj.value };`, '')}
}(this));`;
}(global || this));`;
try {
fs.writeFileSync(GENERATED_CONFIG_FILE_NAME, code, 'utf8');

View File

@@ -3,6 +3,9 @@
"LIQUID_ENABLED": false,
"BISQ_ENABLED": false,
"BISQ_SEPARATE_BACKEND": false,
"ELCTRS_ITEMS_PER_PAGE": 25,
"KEEP_BLOCKS_AMOUNT": 8
}
"ITEMS_PER_PAGE": 10,
"KEEP_BLOCKS_AMOUNT": 8,
"NGINX_PROTOCOL": "http",
"NGINX_HOSTNAME": "127.0.0.1",
"NGINX_PORT": "80"
}

49010
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "mempool-frontend",
"version": "2.0.0",
"version": "2.1.2",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "MIT",
"homepage": "https://mempool.space",
@@ -20,51 +20,67 @@
],
"main": "index.ts",
"scripts": {
"ng": "ng",
"ng": "./node_modules/@angular/cli/bin/ng",
"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",
"build": "npm run generate-config && ng build --prod && npm run sync-assets",
"sync-assets": "node sync-assets.js",
"build": "npm run generate-config && ng build --prod --localize && npm run sync-assets",
"sync-assets": "node sync-assets.js && rsync -av ./dist/mempool/browser/en-US/resources ./dist/mempool/browser/resources",
"sync-assets-dev": "node sync-assets.js dev",
"generate-config": "node generate-config.js",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
"e2e": "ng e2e",
"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"
},
"dependencies": {
"@angular/animations": "~10.0.4",
"@angular/common": "~10.0.4",
"@angular/compiler": "~10.0.4",
"@angular/core": "~10.0.4",
"@angular/forms": "~10.0.4",
"@angular/localize": "^10.0.4",
"@angular/platform-browser": "~10.0.4",
"@angular/platform-browser-dynamic": "~10.0.4",
"@angular/router": "~10.0.4",
"@angular/animations": "~10.2.3",
"@angular/common": "~10.2.3",
"@angular/compiler": "~10.2.3",
"@angular/core": "~10.2.3",
"@angular/forms": "~10.2.3",
"@angular/localize": "^10.2.3",
"@angular/platform-browser": "~10.2.3",
"@angular/platform-browser-dynamic": "~10.2.3",
"@angular/platform-server": "~10.2.2",
"@angular/router": "~10.2.3",
"@fortawesome/angular-fontawesome": "^0.7.0",
"@fortawesome/fontawesome-common-types": "^0.2.30",
"@fortawesome/fontawesome-svg-core": "^1.2.30",
"@fortawesome/free-solid-svg-icons": "^5.14.0",
"@mempool/chartist": "^0.11.4",
"@ng-bootstrap/ng-bootstrap": "^7.0.0",
"@nguniversal/express-engine": "10.1.0",
"@types/qrcode": "^1.3.4",
"bootstrap": "4.5.0",
"chartist": "^0.11.4",
"clipboard": "^2.0.4",
"domino": "^2.1.6",
"express": "^4.15.2",
"ngx-bootrap-multiselect": "^2.0.0",
"ngx-infinite-scroll": "^9.0.0",
"qrcode": "^1.4.4",
"rxjs": "^6.6.0",
"rxjs": "^6.6.3",
"tlite": "^0.1.9",
"tslib": "^2.0.0",
"zone.js": "~0.10.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.1000.3",
"@angular/cli": "~10.0.3",
"@angular/compiler-cli": "~10.0.4",
"@angular/language-service": "~10.0.4",
"@angular-devkit/build-angular": "^0.1002.0",
"@angular/cli": "~10.2.0",
"@angular/compiler-cli": "~10.2.2",
"@angular/language-service": "~10.2.2",
"@nguniversal/builders": "^10.1.0",
"@types/express": "^4.17.0",
"@types/jasmine": "~3.3.8",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^12.11.1",
"codelyzer": "^6.0.0",
"http-proxy-middleware": "^1.0.5",
"jasmine-core": "~3.5.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.0.0",
@@ -75,6 +91,6 @@
"protractor": "~7.0.0",
"ts-node": "~7.0.0",
"tslint": "~6.1.0",
"typescript": "~3.9.7"
"typescript": "~4.0.5"
}
}
}

View File

@@ -1,4 +1,8 @@
{
"/api/v1/donations": {
"target": "http://localhost:9000/",
"secure": false
},
"/api/v1": {
"target": "http://localhost:8999/",
"secure": false
@@ -8,18 +12,63 @@
"secure": false,
"ws": true
},
"/bisq/api": {
"/api/": {
"target": "http://localhost:8999/",
"secure": false,
"pathRewrite": {
"^/bisq/api": "/api/v1/bisq"
"^/api/": "/api/v1/"
}
},
"/api": {
"/testnet/api/v1": {
"target": "http://localhost:8999/",
"secure": false,
"pathRewrite": {
"^/testnet/api/v1": "/api/v1"
}
},
"/testnet/api/v1/ws": {
"target": "http://localhost:8999/",
"secure": false,
"ws": true,
"pathRewrite": {
"^/testnet/api": "/api/v1/ws"
}
},
"/testnet/api/": {
"target": "http://localhost:50001/",
"secure": false,
"pathRewrite": {
"^/api": ""
"^/testnet/api": ""
}
},
"/liquid/api/v1/ws": {
"target": "http://localhost:8999/",
"secure": false,
"ws": true,
"pathRewrite": {
"^/liquid/api": "/api/v1/ws"
}
},
"/liquid/api/": {
"target": "http://localhost:50001/",
"secure": false,
"pathRewrite": {
"^/liquid/api/": ""
}
},
"/bisq/api/": {
"target": "http://localhost:8999/",
"secure": false,
"pathRewrite": {
"^/bisq/api/": "/api/v1/bisq/"
}
},
"/bisq/api/v1/ws": {
"target": "http://localhost:8999/",
"secure": false,
"ws": true,
"pathRewrite": {
"^/bisq/api": "/api/v1/ws"
}
}
}

96
frontend/server.run.ts Normal file
View File

@@ -0,0 +1,96 @@
import 'zone.js/dist/zone-node';
import './generated-config';
import * as domino from 'domino';
import * as express from 'express';
import * as fs from 'fs';
import * as path from 'path';
const {readFileSync, existsSync} = require('fs');
const {createProxyMiddleware} = require('http-proxy-middleware');
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
const win = domino.createWindow(template);
// @ts-ignore
win.__env = global.__env;
// @ts-ignore
win.matchMedia = () => {
return {
matches: true
};
};
// @ts-ignore
win.setTimeout = (fn) => { fn(); };
win.document.body.scrollTo = (() => {});
// @ts-ignore
global['window'] = win;
global['document'] = win.document;
// @ts-ignore
global['history'] = { state: { } };
global['localStorage'] = {
getItem: () => '',
setItem: () => {},
removeItem: () => {},
clear: () => {},
length: 0,
key: () => '',
};
/**
* Return the list of supported and actually active locales
*/
function getActiveLocales() {
const angularConfig = JSON.parse(readFileSync('angular.json', 'utf8'));
const supportedLocales = [
angularConfig.projects.mempool.i18n.sourceLocale,
...Object.keys(angularConfig.projects.mempool.i18n.locales),
];
return supportedLocales.filter(locale => existsSync(`./dist/mempool/server/${locale}`));
}
function app() {
const server = express();
// proxy API to nginx
server.get('/api/**', createProxyMiddleware({
// @ts-ignore
target: win.__env.NGINX_PROTOCOL + '://' + win.__env.NGINX_HOSTNAME + ':' + win.__env.NGINX_PORT,
changeOrigin: true,
}));
// map / and /en to en-US
const defaultLocale = 'en-US';
console.log(`serving default locale: ${defaultLocale}`);
const appServerModule = require(`./dist/mempool/server/${defaultLocale}/main.js`);
server.use('/', appServerModule.app(defaultLocale));
server.use('/en', appServerModule.app(defaultLocale));
// map each locale to its localized main.js
getActiveLocales().forEach(locale => {
console.log('serving locale:', locale);
const appServerModule = require(`./dist/mempool/server/${locale}/main.js`);
// map everything to itself
server.use(`/${locale}`, appServerModule.app(locale));
});
return server;
}
function run() {
const port = process.env.PORT || 4000;
// Start up the Node server
app().listen(port, () => {
console.log(`Node Express server listening on port ${port}`);
});
}
run();

146
frontend/server.ts Normal file
View File

@@ -0,0 +1,146 @@
import 'zone.js/dist/zone-node';
import './generated-config';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import * as fs from 'fs';
import * as path from 'path';
import * as domino from 'domino';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { join } from 'path';
import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
import { existsSync } from 'fs';
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
const win = domino.createWindow(template);
// @ts-ignore
win.__env = global.__env;
// @ts-ignore
win.matchMedia = () => {
return {
matches: true
};
};
// @ts-ignore
win.setTimeout = (fn) => { fn(); };
win.document.body.scrollTo = (() => {});
// @ts-ignore
global['window'] = win;
global['document'] = win.document;
// @ts-ignore
global['history'] = { state: { } };
global['localStorage'] = {
getItem: () => '',
setItem: () => {},
removeItem: () => {},
clear: () => {},
length: 0,
key: () => '',
};
// The Express app is exported so that it can be used by serverless Functions.
export function app(locale: string): express.Express {
const server = express();
const distFolder = join(process.cwd(), `dist/mempool/browser/${locale}`);
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
server.set('view engine', 'html');
server.set('views', distFolder);
// only handle URLs that actually exist
//server.get(locale, getLocalizedSSR(indexHtml));
server.get('/', getLocalizedSSR(indexHtml));
server.get('/tx/*', getLocalizedSSR(indexHtml));
server.get('/block/*', getLocalizedSSR(indexHtml));
server.get('/mempool-block/*', getLocalizedSSR(indexHtml));
server.get('/address/*', getLocalizedSSR(indexHtml));
server.get('/blocks', getLocalizedSSR(indexHtml));
server.get('/graphs', getLocalizedSSR(indexHtml));
server.get('/liquid', getLocalizedSSR(indexHtml));
server.get('/liquid/tx/*', getLocalizedSSR(indexHtml));
server.get('/liquid/block/*', getLocalizedSSR(indexHtml));
server.get('/liquid/mempool-block/*', getLocalizedSSR(indexHtml));
server.get('/liquid/address/*', getLocalizedSSR(indexHtml));
server.get('/liquid/asset/*', getLocalizedSSR(indexHtml));
server.get('/liquid/blocks', getLocalizedSSR(indexHtml));
server.get('/liquid/graphs', getLocalizedSSR(indexHtml));
server.get('/liquid/assets', getLocalizedSSR(indexHtml));
server.get('/liquid/api', getLocalizedSSR(indexHtml));
server.get('/liquid/tv', getLocalizedSSR(indexHtml));
server.get('/liquid/status', getLocalizedSSR(indexHtml));
server.get('/liquid/about', getLocalizedSSR(indexHtml));
server.get('/testnet', getLocalizedSSR(indexHtml));
server.get('/testnet/tx/*', getLocalizedSSR(indexHtml));
server.get('/testnet/block/*', getLocalizedSSR(indexHtml));
server.get('/testnet/mempool-block/*', getLocalizedSSR(indexHtml));
server.get('/testnet/address/*', getLocalizedSSR(indexHtml));
server.get('/testnet/blocks', getLocalizedSSR(indexHtml));
server.get('/testnet/graphs', getLocalizedSSR(indexHtml));
server.get('/testnet/api', getLocalizedSSR(indexHtml));
server.get('/testnet/tv', getLocalizedSSR(indexHtml));
server.get('/testnet/status', getLocalizedSSR(indexHtml));
server.get('/testnet/about', getLocalizedSSR(indexHtml));
server.get('/bisq', getLocalizedSSR(indexHtml));
server.get('/bisq/tx/*', getLocalizedSSR(indexHtml));
server.get('/bisq/blocks', getLocalizedSSR(indexHtml));
server.get('/bisq/block/*', getLocalizedSSR(indexHtml));
server.get('/bisq/address/*', getLocalizedSSR(indexHtml));
server.get('/bisq/stats', getLocalizedSSR(indexHtml));
server.get('/bisq/about', getLocalizedSSR(indexHtml));
server.get('/bisq/api', getLocalizedSSR(indexHtml));
server.get('/about', getLocalizedSSR(indexHtml));
server.get('/api', getLocalizedSSR(indexHtml));
server.get('/tv', getLocalizedSSR(indexHtml));
server.get('/status', getLocalizedSSR(indexHtml));
server.get('/terms-of-service', getLocalizedSSR(indexHtml));
// fallback to static file handler so we send HTTP 404 to nginx
server.get('/**', express.static(distFolder, { maxAge: '1y' }));
return server;
}
function getLocalizedSSR(indexHtml) {
return (req, res) => {
res.render(indexHtml, {
req,
providers: [
{ provide: APP_BASE_HREF, useValue: req.baseUrl }
]
});
}
}
// only used for development mode
function run(): void {
const port = process.env.PORT || 4000;
// Start up the Node server
const server = app('en-US');
server.listen(port, () => {
console.log(`Node Express server listening on port ${port}`);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}
export * from './src/main.server';

View File

@@ -9,10 +9,13 @@ import { AboutComponent } from './components/about/about.component';
import { TelevisionComponent } from './components/television/television.component';
import { StatisticsComponent } from './components/statistics/statistics.component';
import { MempoolBlockComponent } from './components/mempool-block/mempool-block.component';
import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
import { AssetComponent } from './components/asset/asset.component';
import { AssetsComponent } from './assets/assets.component';
import { StatusViewComponent } from './components/status-view/status-view.component';
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';
const routes: Routes = [
{
@@ -25,7 +28,7 @@ const routes: Routes = [
children: [
{
path: '',
component: LatestBlocksComponent
component: DashboardComponent,
},
{
path: 'tx/:id',
@@ -41,6 +44,10 @@ const routes: Routes = [
},
],
},
{
path: 'blocks',
component: LatestBlocksComponent,
},
{
path: 'graphs',
component: StatisticsComponent,
@@ -49,6 +56,14 @@ const routes: Routes = [
path: 'about',
component: AboutComponent,
},
{
path: 'api',
component: ApiDocsComponent,
},
{
path: 'terms-of-service',
component: TermsOfServiceComponent
},
{
path: 'address/:id',
children: [],
@@ -69,7 +84,7 @@ const routes: Routes = [
children: [
{
path: '',
component: LatestBlocksComponent
component: DashboardComponent
},
{
path: 'tx/:id',
@@ -86,12 +101,12 @@ const routes: Routes = [
],
},
{
path: 'graphs',
component: StatisticsComponent,
path: 'blocks',
component: LatestBlocksComponent,
},
{
path: 'about',
component: AboutComponent,
path: 'graphs',
component: StatisticsComponent,
},
{
path: 'address/:id',
@@ -105,6 +120,10 @@ const routes: Routes = [
path: 'assets',
component: AssetsComponent,
},
{
path: 'api',
component: ApiDocsComponent,
},
],
},
{
@@ -134,7 +153,7 @@ const routes: Routes = [
children: [
{
path: '',
component: LatestBlocksComponent
component: DashboardComponent
},
{
path: 'tx/:id',
@@ -151,18 +170,22 @@ const routes: Routes = [
],
},
{
path: 'graphs',
component: StatisticsComponent,
path: 'blocks',
component: LatestBlocksComponent,
},
{
path: 'about',
component: AboutComponent,
path: 'graphs',
component: StatisticsComponent,
},
{
path: 'address/:id',
children: [],
component: AddressComponent
},
{
path: 'api',
component: ApiDocsComponent,
},
],
},
{
@@ -199,7 +222,9 @@ const routes: Routes = [
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
imports: [RouterModule.forRoot(routes, {
initialNavigation: 'enabled'
})],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@@ -34,25 +34,57 @@ export const mempoolFeeColors = [
export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
interface Env {
TESTNET_ENABLED: boolean;
LIQUID_ENABLED: boolean;
BISQ_ENABLED: boolean;
BISQ_SEPARATE_BACKEND: boolean;
ELCTRS_ITEMS_PER_PAGE: number;
KEEP_BLOCKS_AMOUNT: number;
export interface Language {
code: string;
name: string;
}
const defaultEnv: Env = {
'TESTNET_ENABLED': false,
'LIQUID_ENABLED': false,
'BISQ_ENABLED': false,
'BISQ_SEPARATE_BACKEND': false,
'ELCTRS_ITEMS_PER_PAGE': 25,
'KEEP_BLOCKS_AMOUNT': 8
};
const browserWindow = window || {};
// @ts-ignore
const browserWindowEnv = browserWindow.__env || {};
export const env: Env = Object.assign(defaultEnv, browserWindowEnv);
export const languages: Language[] = [
{ code: 'ar', name: 'العربية' }, // Arabic
// { code: 'bg', name: 'Български' }, // Bulgarian
// { code: 'bs', name: 'Bosanski' }, // Bosnian
// { code: 'ca', name: 'Català' }, // Catalan
{ code: 'cs', name: 'Čeština' }, // Czech
// { code: 'da', name: 'Dansk' }, // Danish
{ code: 'de', name: 'Deutsch' }, // German
// { code: 'et', name: 'Eesti' }, // Estonian
// { code: 'el', name: 'Ελληνικά' }, // Greek
{ code: 'en', name: 'English' }, // English
{ code: 'es', name: 'Español' }, // Spanish
// { code: 'eo', name: 'Esperanto' }, // Esperanto
// { code: 'eu', name: 'Euskara' }, // Basque
{ code: 'fa', name: 'فارسی' }, // Persian
{ code: 'fr', name: 'Français' }, // French
// { code: 'gl', name: 'Galego' }, // Galician
{ code: 'ko', name: '한국어' }, // Korean
// { code: 'hr', name: 'Hrvatski' }, // Croatian
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
{ code: 'it', name: 'Italiano' }, // Italian
// { code: 'he', name: 'עברית' }, // Hebrew
{ code: 'ka', name: 'ქართული' }, // Georgian
// { code: 'lv', name: 'Latviešu' }, // Latvian
// { code: 'lt', name: 'Lietuvių' }, // Lithuanian
{ code: 'hu', name: 'Magyar' }, // Hungarian
// { code: 'mk', name: 'Македонски' }, // Macedonian
// { code: 'ms', name: 'Bahasa Melayu' }, // Malay
{ code: 'nl', name: 'Nederlands' }, // Dutch
{ code: 'ja', name: '日本語' }, // Japanese
{ code: 'nb', name: 'Norsk' }, // Norwegian Bokmål
// { code: 'nn', name: 'Norsk Nynorsk' }, // Norwegian Nynorsk
// { 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: 'sk', name: 'Slovenčina' }, // Slovak
{ code: 'sl', name: 'Slovenščina' }, // Slovenian
// { code: 'sr', name: 'Српски / srpski' }, // Serbian
// { code: 'sh', name: 'Srpskohrvatski / српскохрватски' },// Serbo-Croatian
{ code: 'fi', name: 'Suomi' }, // Finnish
{ code: 'sv', name: 'Svenska' }, // Swedish
// { code: 'th', name: 'ไทย' }, // Thai
{ code: 'tr', name: 'Türkçe' }, // Turkish
{ code: 'uk', name: 'Українська' }, // Ukrainian
{ code: 'vi', name: 'Tiếng Việt' }, // Vietnamese
{ code: 'zh', name: '中文' }, // Chinese
];

View File

@@ -1,6 +1,6 @@
import { BrowserModule } from '@angular/platform-browser';
import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
@@ -40,6 +40,15 @@ import { StatusViewComponent } from './components/status-view/status-view.compon
import { MinerComponent } from './components/miner/miner.component';
import { SharedModule } from './shared/shared.module';
import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
import { FeesBoxComponent } from './components/fees-box/fees-box.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faAngleDown, faAngleUp, faBolt, faChartArea, faCogs, faCubes, faDatabase, faExchangeAlt, faInfoCircle,
faLink, faList, faSearch, faTachometerAlt, faThList, faTint, faTv } from '@fortawesome/free-solid-svg-icons';
import { ApiDocsComponent } from './components/api-docs/api-docs.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';
@NgModule({
declarations: [
@@ -56,8 +65,8 @@ import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
TransactionsListComponent,
AddressComponent,
AmountComponent,
SearchFormComponent,
LatestBlocksComponent,
SearchFormComponent,
TimespanComponent,
AddressLabelsComponent,
MempoolBlocksComponent,
@@ -70,14 +79,20 @@ import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
AssetsComponent,
MinerComponent,
StatusViewComponent,
FeesBoxComponent,
DashboardComponent,
ApiDocsComponent,
TermsOfServiceComponent,
],
imports: [
BrowserModule,
BrowserModule.withServerTransition({ appId: 'serverApp' }),
BrowserTransferStateModule,
AppRoutingModule,
HttpClientModule,
BrowserAnimationsModule,
InfiniteScrollModule,
NgbTypeaheadModule,
FontAwesomeModule,
SharedModule,
],
providers: [
@@ -86,7 +101,29 @@ import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
WebsocketService,
AudioService,
SeoService,
StorageService,
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
],
bootstrap: [AppComponent]
})
export class AppModule { }
export class AppModule {
constructor(library: FaIconLibrary) {
library.addIcons(faInfoCircle);
library.addIcons(faChartArea);
library.addIcons(faTv);
library.addIcons(faTachometerAlt);
library.addIcons(faCubes);
library.addIcons(faCogs);
library.addIcons(faThList);
library.addIcons(faList);
library.addIcons(faTachometerAlt);
library.addIcons(faDatabase);
library.addIcons(faSearch);
library.addIcons(faLink);
library.addIcons(faBolt);
library.addIcons(faTint);
library.addIcons(faAngleDown);
library.addIcons(faAngleUp);
library.addIcons(faExchangeAlt);
}
}

View File

@@ -0,0 +1,20 @@
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './components/app/app.component';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
@NgModule({
imports: [
AppModule,
ServerModule,
ServerTransferStateModule,
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
],
bootstrap: [AppComponent],
})
export class AppServerModule {}

View File

@@ -1,26 +1,26 @@
<div class="container-xl">
<h1 style="float: left;">Registered assets</h1>
<h1 style="float: left;" i18n="Registered assets page header">Registered assets</h1>
<br>
<div class="clearfix"></div>
<form [formGroup]="searchForm" class="form-inline">
<div class="input-group m-2">
<input style="width: 250px;" formControlName="searchText" type="text" class="form-control" placeholder="Search asset">
<input style="width: 250px;" formControlName="searchText" type="text" class="form-control" i18n-placeholder="Search Assets Placeholder Text" placeholder="Search asset">
<div class="input-group-append">
<button [disabled]="!searchForm.get('searchText')?.value.length" class="btn btn-secondary" type="button" (click)="searchForm.get('searchText')?.setValue('');" autocomplete="off">Clear</button>
<button [disabled]="!searchForm.get('searchText')?.value.length" class="btn btn-secondary" type="button" (click)="searchForm.get('searchText')?.setValue('');" autocomplete="off" i18n="Search Clear Button">Clear</button>
</div>
</div>
</form>
<ng-template [ngIf]="!isLoading && !error">
<ng-container *ngIf="(assets$ | async) as filteredAssets; else isLoading">
<table class="table table-borderless table-striped">
<thead>
<th class="td-name">Name</th>
<th>Ticker</th>
<th class="d-none d-md-block">Issuer domain</th>
<th>Asset ID</th>
<th class="d-none d-lg-block">Issuance TX</th>
<th class="td-name" i18n="Asset name header">Name</th>
<th i18n="Asset ticker header">Ticker</th>
<th class="d-none d-md-block" i18n="Asset Issuer Domain header">Issuer domain</th>
<th i18n="Asset ID header">Asset ID</th>
<th class="d-none d-lg-block" i18n="Asset issuance transaction header">Issuance TX</th>
</thead>
<tbody>
<tr *ngFor="let asset of filteredAssets; trackBy: trackByAsset">
@@ -37,17 +37,17 @@
<ngb-pagination [collectionSize]="assets.length" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="5" [boundaryLinks]="true"></ngb-pagination>
</ng-template>
</ng-container>
<ng-template [ngIf]="isLoading && !error">
<ng-template #isLoading>
<table class="table table-borderless table-striped">
<thead>
<th>Name</th>
<th>Ticker</th>
<th>Issuer domain</th>
<th>Asset ID</th>
<th>Issuance TX</th>
<th i18n="Asset name header">Name</th>
<th i18n="Asset ticker header">Ticker</th>
<th i18n="Asset Issuer Domain header">Issuer domain</th>
<th i18n="Asset ID header">Asset ID</th>
<th i18n="Asset issuance transaction header">Issuance TX</th>
</thead>
<tbody>
<tr *ngFor="let dummy of [0,0,0]">
@@ -64,7 +64,7 @@
<ng-template [ngIf]="error">
<div class="text-center">
Error loading assets data.
<ng-container i18n="Asset data load error">Error loading assets data.</ng-container>
<br>
<i>{{ error.error }}</i>
</div>

View File

@@ -1,22 +1,26 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { AssetsService } from '../services/assets.service';
import { environment } from 'src/environments/environment';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { distinctUntilChanged } from 'rxjs/operators';
import { distinctUntilChanged, map, filter, mergeMap, tap, take } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router';
import { merge, combineLatest, Observable } from 'rxjs';
import { AssetExtended } from '../interfaces/electrs.interface';
import { SeoService } from '../services/seo.service';
@Component({
selector: 'app-assets',
templateUrl: './assets.component.html',
styleUrls: ['./assets.component.scss']
styleUrls: ['./assets.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssetsComponent implements OnInit {
nativeAssetId = environment.nativeAssetId;
assets: any[];
assetsCache: any[];
filteredAssets: any[];
assets: AssetExtended[];
assetsCache: AssetExtended[];
searchForm: FormGroup;
assets$: Observable<AssetExtended[]>;
isLoading = true;
error: any;
page = 1;
@@ -27,39 +31,28 @@ export class AssetsComponent implements OnInit {
constructor(
private assetsService: AssetsService,
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private seoService: SeoService,
) { }
ngOnInit() {
this.seoService.setTitle($localize`:@@ee8f8008bae6ce3a49840c4e1d39b4af23d4c263:Assets`);
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
this.searchForm = this.formBuilder.group({
searchText: [{ value: '', disabled: true }, Validators.required]
});
this.searchForm.get('searchText').valueChanges
.pipe(
distinctUntilChanged(),
)
.subscribe((searchText) => {
this.page = 1;
if (searchText.length ) {
this.filteredAssets = this.assetsCache.filter((asset) => asset.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1
|| asset.ticker.toLowerCase().indexOf(searchText.toLowerCase()) > -1);
this.assets = this.filteredAssets;
this.filteredAssets = this.filteredAssets.slice(0, this.itemsPerPage);
} else {
this.assets = this.assetsCache;
this.filteredAssets = this.assets.slice(0, this.itemsPerPage);
}
});
this.getAssets();
}
getAssets() {
this.assetsService.getAssetsJson$
.subscribe((assets) => {
this.assets$ = combineLatest([
this.assetsService.getAssetsJson$,
this.route.queryParams
])
.pipe(
take(1),
mergeMap(([assets, qp]) => {
this.assets = Object.values(assets);
// @ts-ignore
this.assets.push({
name: 'Liquid Bitcoin',
ticker: 'L-BTC',
@@ -68,19 +61,92 @@ export class AssetsComponent implements OnInit {
this.assets = this.assets.sort((a: any, b: any) => a.name.localeCompare(b.name));
this.assetsCache = this.assets;
this.searchForm.get('searchText').enable();
this.filteredAssets = this.assets.slice(0, this.itemsPerPage);
this.isLoading = false;
},
(error) => {
console.log(error);
this.error = error;
this.isLoading = false;
});
if (qp.search) {
this.searchForm.get('searchText').setValue(qp.search, { emitEvent: false });
}
return merge(
this.searchForm.get('searchText').valueChanges
.pipe(
distinctUntilChanged(),
tap((text) => {
this.page = 1;
this.searchTextChanged(text);
})
),
this.route.queryParams
.pipe(
filter((queryParams) => {
const newPage = parseInt(queryParams.page, 10);
if (newPage !== this.page || queryParams.search !== this.searchForm.get('searchText').value) {
return true;
}
return false;
}),
map((queryParams) => {
if (queryParams.page) {
const newPage = parseInt(queryParams.page, 10);
this.page = newPage;
} else {
this.page = 1;
}
if (this.searchForm.get('searchText').value !== (queryParams.search || '')) {
this.searchTextChanged(queryParams.search);
}
if (queryParams.search) {
this.searchForm.get('searchText').setValue(queryParams.search, { emitEvent: false });
return queryParams.search;
}
return '';
})
),
);
}),
map((searchText) => {
const start = (this.page - 1) * this.itemsPerPage;
if (searchText.length ) {
const filteredAssets = this.assetsCache.filter((asset) => asset.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1
|| asset.ticker.toLowerCase().indexOf(searchText.toLowerCase()) > -1);
this.assets = filteredAssets;
return filteredAssets.slice(start, this.itemsPerPage + start);
} else {
this.assets = this.assetsCache;
return this.assets.slice(start, this.itemsPerPage + start);
}
})
);
}
pageChange(page: number) {
const start = (page - 1) * this.itemsPerPage;
this.filteredAssets = this.assets.slice(start, this.itemsPerPage + start);
const queryParams = { page: page, search: this.searchForm.get('searchText').value };
if (queryParams.search === '') {
queryParams.search = null;
}
if (queryParams.page === 1) {
queryParams.page = null;
}
this.page = -1;
this.router.navigate([], {
relativeTo: this.route,
queryParams: queryParams,
queryParamsHandling: 'merge',
});
}
searchTextChanged(text: string) {
const queryParams = { search: text, page: 1 };
if (queryParams.search === '') {
queryParams.search = null;
}
if (queryParams.page === 1) {
queryParams.page = null;
}
this.router.navigate([], {
relativeTo: this.route,
queryParams: queryParams,
queryParamsHandling: 'merge',
});
}
trackByAsset(index: number, asset: any) {

View File

@@ -1,5 +1,5 @@
<div class="container-xl">
<h1 style="float: left;">Address</h1>
<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>
@@ -17,15 +17,15 @@
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td>Total received</td>
<td i18n="address.total-received">Total received</td>
<td>{{ totalReceived / 100 | number: '1.2-2' }} BSQ</td>
</tr>
<tr>
<td>Total sent</td>
<td i18n="address.total-sent">Total sent</td>
<td>{{ totalSent / 100 | number: '1.2-2' }} BSQ</td>
</tr>
<tr>
<td>Final balance</td>
<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>
</tr>
</tbody>
@@ -43,7 +43,11 @@
<br>
<h2>{{ transactions.length | number }} transactions</h2>
<h2>
<ng-container *ngTemplateOutlet="transactions.length === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: transactions.length}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
</h2>
<ng-template ngFor let-tx [ngForOf]="transactions">

View File

@@ -36,7 +36,7 @@ export class BisqAddressComponent implements OnInit, OnDestroy {
this.transactions = null;
document.body.scrollTo(0, 0);
this.addressString = params.get('id') || '';
this.seoService.setTitle('Address: ' + this.addressString, true);
this.seoService.setTitle($localize`:@@bisq-address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
return this.bisqApiService.getAddress$(this.addressString)
.pipe(

View File

@@ -1,7 +1,7 @@
<div class="container-xl">
<div class="title-block">
<h1>Block <ng-template [ngIf]="blockHeight"><a [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockHeight }}</a></ng-template></h1>
<h1><ng-template [ngIf]="blockHeight" i18n="block.block">Block <a [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockHeight }}</a></ng-template></h1>
</div>
<div class="clearfix"></div>
@@ -14,15 +14,15 @@
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width">Hash</td>
<td class="td-width" i18n="block.hash">Hash</td>
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" title="{{ block.hash }}">{{ block.hash | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.hash"></app-clipboard></td>
</tr>
<tr>
<td>Timestamp</td>
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
<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> ago)</i>
<i>(<app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since>)</i>
</div>
</td>
</tr>
@@ -32,7 +32,7 @@
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width">Previous hash</td>
<td class="td-width" i18n="block.previous_hash|Transaction Previous Hash">Previous hash</td>
<td><a [routerLink]="['/block/' | relativeUrl, block.previousBlockHash]" title="{{ block.hash }}">{{ block.previousBlockHash | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.previousBlockHash"></app-clipboard></td>
</tr>
</table>
@@ -44,7 +44,11 @@
<br>
<h2>{{ block.txs.length | number }} transactions</h2>
<h2>
<ng-container *ngTemplateOutlet="block.txs.length === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.txs.length| number}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
</h2>
<ng-template ngFor let-tx [ngForOf]="block.txs">
@@ -73,11 +77,11 @@
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width">Hash</td>
<td class="td-width" i18n="block.hash">Hash</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td>Timestamp</td>
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
<td><span class="skeleton-loader"></span></td>
</tr>
</table>
@@ -86,7 +90,7 @@
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width">Previous hash</td>
<td class="td-width" i18n="block.previous_hash|Transaction Previous Hash">Previous hash</td>
<td><span class="skeleton-loader"></span></td>
</tr>
</table>

View File

@@ -82,7 +82,7 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
}
this.isLoading = false;
this.blockHeight = block.height;
this.seoService.setTitle('Block: #' + block.height + ': ' + block.hash, true);
this.seoService.setTitle($localize`:@@bisq-block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.hash}:BLOCK_HASH:`);
this.block = block;
});
}

View File

@@ -1,21 +1,23 @@
<div class="container-xl">
<h1 style="float: left;">Blocks</h1>
<h1 style="float: left;" i18n="Bisq blocks header">Blocks</h1>
<br>
<div class="clearfix"></div>
<ng-container *ngIf="{ value: (blocks$ | async) } as blocks">
<div class="table-responsive-sm">
<table class="table table-borderless table-striped">
<thead>
<th style="width: 25%;">Height</th>
<th style="width: 25%;">Confirmed</th>
<th style="width: 25%;">Total Sent</th>
<th class="d-none d-md-block" style="width: 25%;">Transactions</th>
<th style="width: 25%;" i18n="Bisq block height header">Height</th>
<th style="width: 25%;" i18n="Bisq block confirmed time header">Confirmed</th>
<th style="width: 25%;" i18n="Bisq block total BSQ tokens sent header">Total sent</th>
<th class="d-none d-md-block" style="width: 25%;" i18n="Bisq block transactions title">Transactions</th>
</thead>
<tbody *ngIf="!isLoading; else loadingTmpl">
<tr *ngFor="let block of blocks; trackBy: trackByFn">
<tbody *ngIf="blocks.value; else loadingTmpl">
<tr *ngFor="let block of blocks.value[0]; trackBy: trackByFn">
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
<td><app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since> ago</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 class="d-none d-md-block">{{ block.txs.length }}</td>
</tr>
@@ -25,12 +27,13 @@
<br>
<ngb-pagination [size]="paginationSize" [collectionSize]="totalCount" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true"></ngb-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>
</ng-container>
</div>
<ng-template #loadingTmpl>
<tr *ngFor="let i of loadingItems">
<td *ngFor="let j of [1, 2, 3, 4, 5]"><span class="skeleton-loader"></span></td>
<td *ngFor="let j of [1, 2, 3, 4]"><span class="skeleton-loader"></span></td>
</tr>
</ng-template>

View File

@@ -1,18 +1,19 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { BisqApiService } from '../bisq-api.service';
import { switchMap, tap } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { switchMap, map, take, mergeMap, tap } from 'rxjs/operators';
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';
@Component({
selector: 'app-bisq-blocks',
templateUrl: './bisq-blocks.component.html',
styleUrls: ['./bisq-blocks.component.scss']
styleUrls: ['./bisq-blocks.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BisqBlocksComponent implements OnInit {
blocks: BisqBlock[];
totalCount: number;
blocks$: Observable<[BisqBlock[], number]>;
page = 1;
itemsPerPage: number;
contentSpace = window.innerHeight - (165 + 75);
@@ -23,15 +24,15 @@ export class BisqBlocksComponent implements OnInit {
paginationSize: 'sm' | 'lg' = 'md';
paginationMaxSize = 10;
pageSubject$ = new Subject<number>();
constructor(
private bisqApiService: BisqApiService,
private seoService: SeoService,
private route: ActivatedRoute,
private router: Router,
) { }
ngOnInit(): void {
this.seoService.setTitle('Blocks', true);
this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
this.loadingItems = Array(this.itemsPerPage);
if (document.body.clientWidth < 768) {
@@ -39,20 +40,28 @@ export class BisqBlocksComponent implements OnInit {
this.paginationMaxSize = 3;
}
this.pageSubject$
this.blocks$ = this.route.queryParams
.pipe(
tap(() => this.isLoading = true),
switchMap((page) => this.bisqApiService.listBlocks$((page - 1) * this.itemsPerPage, this.itemsPerPage))
)
.subscribe((response) => {
this.isLoading = false;
this.blocks = response.body;
this.totalCount = parseInt(response.headers.get('x-total-count'), 10);
}, (error) => {
console.log(error);
});
this.pageSubject$.next(1);
take(1),
tap((qp) => {
if (qp.page) {
this.page = parseInt(qp.page, 10);
}
}),
mergeMap(() => this.route.queryParams),
map((queryParams) => {
if (queryParams.page) {
const newPage = parseInt(queryParams.page, 10);
this.page = newPage;
return newPage;
} else {
this.page = 1;
}
return 1;
}),
switchMap((page) => this.bisqApiService.listBlocks$((page - 1) * this.itemsPerPage, this.itemsPerPage)),
map((response) => [response.body, parseInt(response.headers.get('x-total-count'), 10)]),
);
}
calculateTotalOutput(block: BisqBlock): number {
@@ -66,6 +75,9 @@ export class BisqBlocksComponent implements OnInit {
}
pageChange(page: number) {
this.pageSubject$.next(page);
this.router.navigate([], {
queryParams: { page: page },
queryParamsHandling: 'merge',
});
}
}

View File

@@ -73,9 +73,15 @@ export class BisqIconComponent implements OnChanges {
this.iconProp[1] = 'file-alt';
this.color = '6c8b3b';
break;
case 'IRREGULAR':
this.iconProp[1] = 'exclamation-circle';
this.color = 'ffd700';
break;
default:
this.iconProp[1] = 'question';
this.color = 'ffac00';
}
// @ts-ignore
this.iconProp = this.iconProp.slice();
}
}

View File

@@ -1,5 +1,5 @@
<div class="container-xl">
<h1 style="float: left;">BSQ Statistics</h1>
<h1 style="float: left;" i18n="BSQ statistics header">BSQ statistics</h1>
<br>
<div class="clearfix"></div>
@@ -7,42 +7,38 @@
<div class="row">
<div class="col-sm">
<table class="table table-borderless table-striped">
<thead>
<th>Property</th>
<th>Value</th>
</thead>
<tbody *ngIf="!isLoading; else loadingTemplate">
<tr>
<td class="td-width">Existing amount</td>
<td>{{ (stats.minted - stats.burnt) / 100 | number: '1.2-2' }} BSQ</td>
<td class="td-width" i18n="BSQ existing amount">Existing amount</td>
<td>{{ (stats.minted - stats.burnt) | number: '1.2-2' }} BSQ</td>
</tr>
<tr>
<td>Minted amount</td>
<td i18n="BSQ minted amount">Minted amount</td>
<td>{{ stats.minted | number: '1.2-2' }} BSQ</td>
</tr>
<tr>
<td>Burnt amount</td>
<td i18n="BSQ burnt amount">Burnt amount</td>
<td>{{ stats.burnt | number: '1.2-2' }} BSQ</td>
</tr>
<tr>
<td>Addresses</td>
<td i18n="BSQ addresses">Addresses</td>
<td>{{ stats.addresses | number }}</td>
</tr>
<tr>
<td>Unspent TXOs</td>
<td i18n="BSQ unspent transaction outputs">Unspent TXOs</td>
<td>{{ stats.unspent_txos | number }}</td>
</tr>
<tr>
<td>Spent TXOs</td>
<td i18n="BSQ spent transaction outputs">Spent TXOs</td>
<td>{{ stats.spent_txos | number }}</td>
</tr>
<tr>
<td>Price</td>
<td i18n="BSQ token price">Price</td>
<td><app-fiat [value]="price"></app-fiat></td>
</tr>
<tr>
<td>Market cap</td>
<td><app-fiat [value]="price * (stats.minted - stats.burnt) / 100"></app-fiat></td>
<td i18n="BSQ token market cap">Market cap</td>
<td><app-fiat [value]="price * (stats.minted - stats.burnt)"></app-fiat></td>
</tr>
</tbody>
</table>
@@ -55,23 +51,23 @@
<ng-template #loadingTemplate>
<tbody>
<tr>
<td class="td-width">Existing amount</td>
<td class="td-width" i18n>Existing amount</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td>Minted amount</td>
<td i18n>Minted amount</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td>Burnt amount</td>
<td i18n>Burnt amount</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td>Addresses</td>
<td i18n>Addresses</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td>Unspent TXOs</td>
<td i18n>Unspent TXOs</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
@@ -79,12 +75,12 @@
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td>Price</td>
<td i18n>Price</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td>Market cap</td>
<td i18n>Market cap</td>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</ng-template>
</ng-template>

View File

@@ -21,8 +21,7 @@ export class BisqStatsComponent implements OnInit {
) { }
ngOnInit() {
this.seoService.setTitle('BSQ Statistics', false);
this.seoService.setTitle($localize`:@@2a30a4cdb123a03facc5ab8c5b3e6d8b8dbbc3d4:BSQ statistics`);
this.stateService.bsqPrice$
.subscribe((bsqPrice) => {
this.price = bsqPrice;

View File

@@ -4,15 +4,15 @@
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width">Inputs</td>
<td class="td-width" i18n="transaction.inputs">Inputs</td>
<td>{{ totalInput / 100 | number: '1.2-2' }} BSQ</td>
</tr>
<tr>
<td>Outputs</td>
<td i18n="transaction.outputs">Outputs</td>
<td>{{ totalOutput / 100 | number: '1.2-2' }} BSQ</td>
</tr>
<tr>
<td>Issuance</td>
<td i18n="asset.issued-amount|Liquid Asset issued amount">Issued amount</td>
<td>{{ totalIssued / 100 | number: '1.2-2' }} BSQ</td>
</tr>
</tbody>
@@ -22,11 +22,11 @@
<table class="table table-borderless table-striped">
<tbody class="mobile-even">
<tr>
<td class="td-width">Type</td>
<td class="td-width" i18n>Type</td>
<td><app-bisq-icon class="mr-1" [txType]="tx.txType"></app-bisq-icon> {{ tx.txTypeDisplayString }}</td>
</tr>
<tr>
<td>Version</td>
<td i18n="transaction.version">Version</td>
<td>{{ tx.txVersion }}</td>
</tr>
</tbody>

View File

@@ -1,11 +1,14 @@
<div class="container-xl">
<h1 class="float-left mr-3 mb-md-3">Transaction</h1>
<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">{{ latestBlock.height - bisqTx.blockHeight + 1 }} confirmation<ng-container *ngIf="latestBlock.height - bisqTx.blockHeight + 1 > 1">s</ng-container></button>
<button *ngIf="(latestBlock$ | async) as latestBlock" type="button" class="btn btn-sm btn-success float-right mr-2 mt-1 mt-md-3">
<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;">
<span class="d-inline d-lg-none">{{ bisqTx.id | shortenString : 24 }}</span>
@@ -21,14 +24,22 @@
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width">Included in block</td>
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
<td>
<a [routerLink]="['/block/' | relativeUrl, bisqTx.blockHash]" [state]="{ data: { blockHeight: bisqTx.blockHeight } }">{{ bisqTx.blockHeight }}</a>
<i> (<app-time-since [time]="bisqTx.time / 1000" [fastRender]="true"></app-time-since> ago)</i>
{{ 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>
</div>
</td>
</tr>
<tr>
<td class="td-width">Features</td>
<td class="td-width" i18n="transaction.included-in-block|Transaction included in block">Included in block</td>
<td>
<a [routerLink]="['/block/' | relativeUrl, bisqTx.blockHash]" [state]="{ data: { blockHeight: bisqTx.blockHeight } }">{{ bisqTx.blockHeight }}</a>
</td>
</tr>
<tr>
<td class="td-width" i18n="transaction.features|Transaction features">Features</td>
<td>
<app-tx-features *ngIf="tx; else loadingTx" [tx]="tx"></app-tx-features>
<ng-template #loadingTx>
@@ -43,12 +54,12 @@
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width">Burnt</td>
<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>)
</tr>
<tr>
<td>Fee per vByte</td>
<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
&nbsp;
@@ -67,14 +78,14 @@
<br>
<h2>Details</h2>
<h2 i18n="transaction.details">Details</h2>
<app-bisq-transaction-details [tx]="bisqTx"></app-bisq-transaction-details>
<br>
<h2>Inputs & Outputs</h2>
<h2 i18n="transaction.inputs-and-outputs|Transaction inputs and outputs">Inputs & Outputs</h2>
<app-bisq-transfers [tx]="bisqTx"></app-bisq-transfers>
@@ -113,7 +124,7 @@
<br>
<h2>Details</h2>
<h2 i18n="transaction.details">Details</h2>
<div class="box">
<table class="table table-borderless table-striped">
<tbody>
@@ -134,7 +145,7 @@
<br>
<h2>Inputs & Outputs</h2>
<h2 i18n="transaction.inputs-and-outputs|Transaction inputs and outputs">Inputs & Outputs</h2>
<div class="box">
<div class="row">
@@ -155,7 +166,7 @@
<div class="clearfix"></div>
<div class="text-center">
Error loading transaction
Error loading Bisq transaction
<br><br>
<i>{{ error.status }}: {{ error.statusText }}</i>
</div>

View File

@@ -43,7 +43,7 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
this.error = null;
document.body.scrollTo(0, 0);
this.txId = params.get('id') || '';
this.seoService.setTitle('Transaction: ' + this.txId, true);
this.seoService.setTitle($localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`);
if (history.state.data) {
return of(history.state.data);
}

View File

@@ -1,75 +1,28 @@
<div class="container-xl">
<h1 style="float: left;">Transactions</h1>
<h1 style="float: left;" i18n>Transactions</h1>
<div ngbDropdown class="d-block float-right">
<button class="btn btn-primary" id="dropdownForm1" ngbDropdownToggle>Filter</button>
<div ngbDropdownMenu aria-labelledby="dropdownForm1">
<form [formGroup]="radioGroupForm">
<label>
<input type="checkbox" formControlName="ASSET_LISTING_FEE"> Asset listing fee
</label>
<br>
<label>
<input type="checkbox" formControlName="BLIND_VOTE"> Blind vote
</label>
<br>
<label>
<input type="checkbox" formControlName="COMPENSATION_REQUEST"> Compensation request
</label>
<br>
<label>
<input type="checkbox" formControlName="GENESIS"> Genesis
</label>
<br>
<label>
<input type="checkbox" formControlName="LOCKUP"> Lockup
</label>
<br>
<label>
<input type="checkbox" formControlName="PAY_TRADE_FEE"> Pay trade fee
</label>
<br>
<label>
<input type="checkbox" formControlName="PROOF_OF_BURN"> Proof of burn
</label>
<br>
<label>
<input type="checkbox" formControlName="PROPOSAL"> Proposal
</label>
<br>
<label>
<input type="checkbox" formControlName="REIMBURSEMENT_REQUEST"> Reimbursement request
</label>
<br>
<label>
<input type="checkbox" formControlName="TRANSFER_BSQ"> Transfer BSQ
</label>
<br>
<label>
<input type="checkbox" formControlName="UNLOCK"> Unlock
</label>
<br>
<label>
<input type="checkbox" formControlName="VOTE_REVEAL"> Vote reveal
</label>
</form>
</div>
<div class="d-block float-right">
<form [formGroup]="radioGroupForm">
<ngx-bootrap-multiselect [options]="txTypeOptions" [settings]="txTypeDropdownSettings" [texts]="txTypeDropdownTexts" formControlName="txTypes"></ngx-bootrap-multiselect>
</form>
</div>
<br>
<div class="clearfix"></div>
<ng-container *ngIf="{ value: (transactions$ | async) } as transactions">
<table class="table table-borderless table-striped">
<thead>
<th style="width: 20%;">Transaction</th>
<th class="d-none d-md-block" style="width: 20%;">Type</th>
<th style="width: 20%;">Amount</th>
<th style="width: 20%;">Confirmed</th>
<th class="d-none d-md-block" style="width: 20%;">Height</th>
<th style="width: 20%;" i18n>Transaction</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>
<th class="d-none d-md-block" i18n>Height</th>
</thead>
<tbody *ngIf="!isLoading; else loadingTmpl">
<tr *ngFor="let tx of transactions; trackBy: trackByFn">
<tbody *ngIf="transactions.value; else loadingTmpl">
<tr *ngFor="let tx of transactions.value[0]; trackBy: trackByFn">
<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>
@@ -77,14 +30,14 @@
</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'" [ngIfElse]="defaultTxType">
<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>
</ng-template>
<ng-template #defaultTxType>
{{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }}<span class="d-none d-md-inline"> BSQ</span>
</ng-template>
</td>
<td><app-time-since [time]="tx.time / 1000" [fastRender]="true"></app-time-since> ago</td>
<td><app-time-since [time]="tx.time / 1000" [fastRender]="true"></app-time-since></td>
<td class="d-none d-md-block"><a [routerLink]="['/block/' | relativeUrl, tx.blockHash]" [state]="{ data: { blockHeight: tx.blockHeight } }">{{ tx.blockHeight }}</a></td>
</tr>
</tbody>
@@ -92,8 +45,9 @@
<br>
<ngb-pagination [size]="paginationSize" [collectionSize]="totalCount" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true"></ngb-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>
</ng-container>
</div>
<ng-template #loadingTmpl>

View File

@@ -2,3 +2,8 @@ label {
padding: 0.25rem 1rem;
white-space: nowrap;
}
:host ::ng-deep .dropdown-menu {
right: 0px;
left: inherit;
}

View File

@@ -1,60 +1,85 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { BisqTransaction, BisqOutput } from '../bisq.interfaces';
import { Subject, merge } from 'rxjs';
import { switchMap, tap, map } from 'rxjs/operators';
import { merge, Observable } from 'rxjs';
import { switchMap, map, tap, filter } from 'rxjs/operators';
import { BisqApiService } from '../bisq-api.service';
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';
@Component({
selector: 'app-bisq-transactions',
templateUrl: './bisq-transactions.component.html',
styleUrls: ['./bisq-transactions.component.scss']
styleUrls: ['./bisq-transactions.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BisqTransactionsComponent implements OnInit {
transactions: BisqTransaction[];
totalCount: number;
transactions$: Observable<[BisqTransaction[], number]>;
page = 1;
itemsPerPage: number;
contentSpace = window.innerHeight - (165 + 75);
itemsPerPage = 50;
fiveItemsPxSize = 250;
isLoading = true;
loadingItems: number[];
pageSubject$ = new Subject<number>();
radioGroupForm: FormGroup;
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' },
];
txTypesDefaultChecked = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
txTypeDropdownSettings: IMultiSelectSettings = {
buttonClasses: 'btn btn-primary btn-sm',
displayAllSelectedText: true,
showCheckAll: true,
showUncheckAll: true,
maxHeight: '500px',
fixedTitle: true,
};
txTypeDropdownTexts: IMultiSelectTexts = {
defaultTitle: $localize`:@@bisq-transactions.filter:Filter`,
checkAll: $localize`:@@bisq-transactions.selectall:Select all`,
uncheckAll: $localize`:@@bisq-transactions.unselectall:Unselect all`,
};
// @ts-ignore
paginationSize: 'sm' | 'lg' = 'md';
paginationMaxSize = 10;
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 bisqApiService: BisqApiService,
private seoService: SeoService,
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private cd: ChangeDetectorRef,
) { }
ngOnInit(): void {
this.seoService.setTitle('Transactions', true);
this.seoService.setTitle($localize`:@@add4cd82e3e38a3110fe67b3c7df56e9602644ee:Transactions`);
this.radioGroupForm = this.formBuilder.group({
UNVERIFIED: false,
INVALID: false,
GENESIS: false,
TRANSFER_BSQ: false,
PAY_TRADE_FEE: false,
PROPOSAL: false,
COMPENSATION_REQUEST: false,
REIMBURSEMENT_REQUEST: false,
BLIND_VOTE: false,
VOTE_REVEAL: false,
LOCKUP: false,
UNLOCK: false,
ASSET_LISTING_FEE: false,
PROOF_OF_BURN: false,
txTypes: [this.txTypesDefaultChecked],
});
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
this.loadingItems = Array(this.itemsPerPage);
if (document.body.clientWidth < 768) {
@@ -62,39 +87,70 @@ export class BisqTransactionsComponent implements OnInit {
this.paginationMaxSize = 3;
}
merge(
this.pageSubject$,
this.transactions$ = merge(
this.route.queryParams
.pipe(
filter((queryParams) => {
const newPage = parseInt(queryParams.page, 10);
const types = queryParams.types;
if (newPage !== this.page || types !== this.types.map((type) => this.txTypes.indexOf(type) + 1).join(',')) {
return true;
}
return false;
}),
tap((queryParams) => {
if (queryParams.page) {
const newPage = parseInt(queryParams.page, 10);
this.page = newPage;
} else {
this.page = 1;
}
if (queryParams.types) {
const types = queryParams.types.split(',').map((str: string) => parseInt(str, 10));
this.types = types.map((id: number) => this.txTypes[id - 1]);
this.radioGroupForm.get('txTypes').setValue(types, { emitEvent: false });
} else {
this.types = [];
this.radioGroupForm.get('txTypes').setValue(this.txTypesDefaultChecked, { emitEvent: false });
}
this.cd.markForCheck();
})
),
this.radioGroupForm.valueChanges
.pipe(
map((data) => {
const types: string[] = [];
for (const i in data) {
if (data[i]) {
types.push(i);
}
tap((data) => {
this.types = data.txTypes.map((id: number) => this.txTypes[id - 1]);
if (this.types.length === this.txTypes.length) {
this.types = [];
}
this.types = types;
return 1;
this.page = 1;
this.typesChanged(data.txTypes);
this.cd.markForCheck();
})
)
),
)
.pipe(
tap(() => this.isLoading = true),
switchMap((page) => this.bisqApiService.listTransactions$((page - 1) * this.itemsPerPage, this.itemsPerPage, this.types))
)
.subscribe((response) => {
this.isLoading = false;
this.transactions = response.body;
this.totalCount = parseInt(response.headers.get('x-total-count'), 10);
}, (error) => {
console.log(error);
});
this.pageSubject$.next(1);
switchMap(() => this.bisqApiService.listTransactions$((this.page - 1) * this.itemsPerPage, this.itemsPerPage, this.types)),
map((response) => [response.body, parseInt(response.headers.get('x-total-count'), 10)])
);
}
pageChange(page: number) {
this.pageSubject$.next(page);
this.router.navigate([], {
relativeTo: this.route,
queryParams: { page: page },
queryParamsHandling: 'merge',
});
// trigger queryParams change
this.page = -1;
}
typesChanged(types: number[]) {
this.router.navigate([], {
relativeTo: this.route,
queryParams: { types: types.join(','), page: 1 },
queryParamsHandling: 'merge',
});
}
calculateTotalOutput(outputs: BisqOutput[]): number {

View File

@@ -59,12 +59,16 @@
<div>
<div class="float-left mt-2-5" *ngIf="showConfirmations && tx.burntFee">
Burnt: {{ tx.burntFee / 100 | number: '1.2-2' }} BSQ (<app-bsq-amount [bsq]="tx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount>)
<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>
<div class="float-right">
<span *ngIf="showConfirmations && latestBlock$ | async as latestBlock">
<button type="button" class="btn btn-sm btn-success mt-2">{{ latestBlock.height - tx.blockHeight + 1 }} confirmation<ng-container *ngIf="latestBlock.height - tx.blockHeight + 1 > 1">s</ng-container></button>
<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>
<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>
&nbsp;
</span>
<button type="button" class="btn btn-sm btn-primary mt-2" (click)="switchCurrency()">

View File

@@ -1,6 +1,8 @@
import { NgModule } from '@angular/core';
import { BisqRoutingModule } from './bisq.routing.module';
import { SharedModule } from '../shared/shared.module';
import { NgxBootstrapMultiselectModule } from 'ngx-bootrap-multiselect';
import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component';
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component';
@@ -10,7 +12,7 @@ import { BisqTransactionDetailsComponent } from './bisq-transaction-details/bisq
import { BisqTransfersComponent } from './bisq-transfers/bisq-transfers.component';
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faLeaf, faQuestion, faExclamationTriangle, faRocket, faRetweet, faFileAlt, faMoneyBill,
faEye, faEyeSlash, faLock, faLockOpen } from '@fortawesome/free-solid-svg-icons';
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';
@@ -38,6 +40,7 @@ import { BsqAmountComponent } from './bsq-amount/bsq-amount.component';
SharedModule,
NgbPaginationModule,
FontAwesomeModule,
NgxBootstrapMultiselectModule,
],
providers: [
BisqApiService,
@@ -46,6 +49,7 @@ import { BsqAmountComponent } from './bsq-amount/bsq-amount.component';
export class BisqModule {
constructor(library: FaIconLibrary) {
library.addIcons(faQuestion);
library.addIcons(faExclamationCircle);
library.addIcons(faExclamationTriangle);
library.addIcons(faRocket);
library.addIcons(faRetweet);

View File

@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AboutComponent } from '../components/about/about.component';
import { AddressComponent } from '../components/address/address.component';
import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component';
import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component';
import { BisqBlockComponent } from './bisq-block/bisq-block.component';
@@ -9,6 +8,7 @@ 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';
const routes: Routes = [
{
@@ -44,6 +44,10 @@ const routes: Routes = [
path: 'about',
component: AboutComponent,
},
{
path: 'api',
component: ApiDocsComponent,
},
{
path: '**',
redirectTo: ''

View File

@@ -63,6 +63,68 @@ export function calcSegwitFeeGains(tx: Transaction) {
};
}
// https://github.com/shesek/move-decimal-point
export function moveDec(num: number, n: number) {
let frac, int, neg, ref;
if (n === 0) {
return num;
}
ref = ('' + num).split('.'), int = ref[0], frac = ref[1];
int || (int = '0');
frac || (frac = '0');
neg = (int[0] === '-' ? '-' : '');
if (neg) {
int = int.slice(1);
}
if (n > 0) {
if (n > frac.length) {
frac += zeros(n - frac.length);
}
int += frac.slice(0, n);
frac = frac.slice(n);
} else {
n = n * -1;
if (n > int.length) {
int = (zeros(n - int.length)) + int;
}
frac = int.slice(n * -1) + frac;
int = int.slice(0, n * -1);
}
while (int[0] === '0') {
int = int.slice(1);
}
while (frac[frac.length - 1] === '0') {
frac = frac.slice(0, -1);
}
return neg + (int || '0') + (frac.length ? '.' + frac : '');
}
function zeros(n) {
return new Array(n + 1).join('0');
}
// Formats a number for display. Treats the number as a string to avoid rounding errors.
export const formatNumber = (s, precision = null) => {
let [ whole, dec ] = s.toString().split('.');
// divide numbers into groups of three separated with a thin space (U+202F, "NARROW NO-BREAK SPACE"),
// but only when there are more than a total of 5 non-decimal digits.
if (whole.length >= 5) {
whole = whole.replace(/\B(?=(\d{3})+(?!\d))/g, '\u202F');
}
if (precision != null && precision > 0) {
if (dec == null) {
dec = '0'.repeat(precision);
}
else if (dec.length < precision) {
dec += '0'.repeat(precision - dec.length);
}
}
return whole + (dec != null ? '.' + dec : '');
};
// Utilities for segwitFeeGains
const witnessSize = (vin: Vin) => vin.witness.reduce((S, w) => S + (w.length / 2), 0);
const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0;

View File

@@ -1,117 +1,214 @@
<div class="container-xl">
<div class="text-center">
<br />
<img src="./resources/mempool-tube.png" width="63" height="63" />
<br /><br />
<br>
<img src="./resources/mempool-logo-bigger.png" height="62.5" width="250">
<br>
<h2>Contributors</h2>
<div class="text-small text-center offset-md-1">
v2.1.2 ({{ gitCommit$ | async }})
</div>
<p>Development <a href="https://twitter.com/softsimon_">@softsimon_</a>
<br />Operations <a href="https://twitter.com/wiz">@wiz</a>
<br />Logo & theme design <a href="https://instagram.com/markjborg">@markjborg</a>
<br>
<h2 i18n="about.about-the-project">The Mempool Open Source Project</h2>
<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 any ads, altcoins, or third-party trackers.</p>
</div>
</div>
<br>
<h2 i18n="about.maintainers">Maintainers</h2>
<div class="container text-center">
<div class="row row-cols-2" dir="ltr">
<div class="col col-md-2 offset-md-4">
<a href="https://twitter.com/softsimon_">
<div class="profile_photo mx-auto" style="background-image: url(/resources/profile_softsimon.jpg)"></div>
@softsimon_
</a>
<br>
<span i18n="about.development">Development</span>
</div>
<div class="col col-md-2">
<a href="https://twitter.com/wiz">
<div class="profile_photo mx-auto" style="background-image: url(/resources/profile_wiz.png)"></div>
@wiz
</a>
<br>
<span i18n="about.operations">Operations</span>
</div>
</div>
</div>
<br><br>
<h2>Open source</h2>
<h2 i18n="about.sponsors.withHeart">Sponsors ❤️</h2>
<a target="_blank" class="b2812e30 f2874b88 fw6 mb3 mt2 truncate black-80 f4 link" rel="noopener noreferrer nofollow" href="https://github.com/mempool/mempool">
<span class="_9e13d83d dib v-mid">
<svg style="height: 16px;margin-right: 8px;" viewBox="0 0 92 92" version="1.1" xmlns="http://www.w3.org/2000/svg">
<title>Git</title>
<g stroke="none" fill="#FFFFFF">
<path d="M90.155,41.965 L50.036,1.847 C47.726,-0.464 43.979,-0.464 41.667,1.847 L33.336,10.179 L43.904,20.747 C46.36,19.917 49.176,20.474 51.133,22.431 C53.102,24.401 53.654,27.241 52.803,29.706 L62.989,39.891 C65.454,39.041 68.295,39.59 70.264,41.562 C73.014,44.311 73.014,48.768 70.264,51.519 C67.512,54.271 63.056,54.271 60.303,51.519 C58.235,49.449 57.723,46.409 58.772,43.861 L49.272,34.362 L49.272,59.358 C49.942,59.69 50.575,60.133 51.133,60.69 C53.883,63.44 53.883,67.896 51.133,70.65 C48.383,73.399 43.924,73.399 41.176,70.65 C38.426,67.896 38.426,63.44 41.176,60.69 C41.856,60.011 42.643,59.497 43.483,59.153 L43.483,33.925 C42.643,33.582 41.858,33.072 41.176,32.389 C39.093,30.307 38.592,27.249 39.661,24.691 L29.243,14.271 L1.733,41.779 C-0.578,44.092 -0.578,47.839 1.733,50.15 L41.854,90.268 C44.164,92.578 47.91,92.578 50.223,90.268 L90.155,50.336 C92.466,48.025 92.466,44.275 90.155,41.965"></path>
</g>
</svg>
<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 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>
</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>
</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>
<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-3x" 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>
<span>github.com/mempool/mempool</span></a>
</a>
<a target="_blank" class="m-2 fw6 mb3 mt2 truncate black-80 f4 link" href="https://twitter.com/mempoolspace">
<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-3x" 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-3x" 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-3x" 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>
</div>
<br><br>
<div class="text-center">
<h2>API</h2>
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
</div>
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-tabs">
<li [ngbNavItem]="1">
<a ngbNavLink>Mainnet</a>
<ng-template ngbNavContent>
<table class="table">
<tr>
<th style="border-top: 0;">Endpoint</th>
<th style="border-top: 0;">Description</th>
</tr>
<tr>
<td class="nowrap"><a href="/api/v1/fees/recommended" target="_blank">GET /api/v1/fees/recommended</a></td>
<td>Recommended fees</td>
</tr>
<tr>
<td class="nowrap"><a href="/api/v1/fees/mempool-blocks" target="_blank">GET /api/v1/fees/mempool-blocks</a></td>
<td>The current mempool blocks</td>
</tr>
<tr>
<td class="nowrap">wss://{{ hostname }}/api/v1/ws</td>
<td>
<span class="text-small">
Default push: <span class="code">{{ '{' }} action: 'want', data: ['blocks', ...] {{ '}' }}</span>
to express what you want pushed. Available: 'blocks', 'mempool-blocks', 'live-2h-chart' and 'stats'.
</span>
<br><br>
<span class="text-small">
Push transactions related to address: <span class="code">{{ '{' }} 'track-address': '3PbJ...bF9B' {{ '}' }}</span>
to receive all new transactions containing that address as input or output. Returns an array of transactions. 'address-transactions' for new mempool transactions and 'block-transactions' for new block confirmed transactions.
</span>
</td>
</tr>
</table>
</ng-template>
</li>
<li [ngbNavItem]="2">
<a ngbNavLink>Bisq</a>
<ng-template ngbNavContent>
<table class="table">
<tr>
<th style="border-top: 0;">Endpoint</th>
<th style="border-top: 0;">Description</th>
</tr>
<tr>
<td class="nowrap"><a href="/bisq/api/stats" target="_blank">GET /bisq/api/stats</a></td>
<td>Stats</td>
</tr>
<tr>
<td class="nowrap"><a href="/bisq/api/tx/4b5417ec5ab6112bedf539c3b4f5a806ed539542d8b717e1c4470aa3180edce5" target="_blank">GET /bisq/api/tx/:txId</a></td>
<td>Transaction</td>
</tr>
<tr>
<td class="nowrap"><a href="/bisq/api/txs/0/25" target="_blank">GET /bisq/api/txs/:index/:length</a></td>
<td>Transactions</td>
</tr>
<tr>
<td class="nowrap"><a href="/bisq/api/block/000000000000000000079aa6bfa46eb8fc20474e8673d6e8a123b211236bf82d" target="_blank">GET /bisq/api/block/:hash</a></td>
<td>Block</td>
</tr>
<tr>
<td class="nowrap"><a href="/bisq/api/blocks/0/25" target="_blank">GET /bisq/api/blocks/:index/:length</a></td>
<td>Blocks</td>
</tr>
<tr>
<td class="nowrap"><a href="/bisq/api/blocks/tip/height" target="_blank">GET /bisq/api/blocks/tip/height</a></td>
<td>Latest block height</td>
</tr>
<tr>
<td class="nowrap"><a href="/bisq/api/address/B1DgwRN92rdQ9xpEVCdXRfgeqGw9X4YtrZz" target="_blank">GET /bisq/api/address/:address</a></td>
<td>Address</td>
</tr>
</table>
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="nav" class="mt-2"></div>
<br> <br>
</div>

View File

@@ -1,16 +1,35 @@
.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: 10px;
}
.profile_img {
width: 80px;
height: 80px;
border-radius: 50%;
border: 0;
}
.text-small {
font-size: 12px;
}
.code {
background-color: #1d1f31;
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New;
.info-group {
max-width: 400px;
}
tr {
white-space: inherit;
}
.nowrap {
white-space: nowrap;
}
.required {
color: #FF0000;
font-weight: bold;
}

View File

@@ -1,32 +1,91 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnDestroy, 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 { ApiService } from 'src/app/services/api.service';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { delay, map, retryWhen, switchMap, tap } from 'rxjs/operators';
@Component({
selector: 'app-about',
templateUrl: './about.component.html',
styleUrls: ['./about.component.scss']
styleUrls: ['./about.component.scss'],
})
export class AboutComponent implements OnInit {
active = 1;
hostname = document.location.hostname;
export class AboutComponent implements OnInit, OnDestroy {
gitCommit$: Observable<string>;
donationForm: FormGroup;
paymentForm: FormGroup;
donationStatus = 1;
sponsors$: Observable<any>;
donationObj: any;
sponsorsEnabled = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
sponsors = null;
requestSubscription: Subscription | undefined;
constructor(
private websocketService: WebsocketService,
private seoService: SeoService,
private stateService: StateService,
private formBuilder: FormBuilder,
private apiService: ApiService,
private sanitizer: DomSanitizer,
) { }
ngOnInit() {
this.seoService.setTitle('Contributors');
this.gitCommit$ = this.stateService.gitCommit$.pipe(map((str) => str.substr(0, 8)));
this.seoService.setTitle($localize`:@@004b222ff9ef9dd4771b777950ca1d0e4cd4348a:About`);
this.websocketService.want(['blocks']);
if (this.stateService.network === 'bisq') {
this.active = 2;
}
if (document.location.port !== '') {
this.hostname = this.hostname + ':' + document.location.port;
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;
});
}
ngOnDestroy() {
if (this.requestSubscription) {
this.requestSubscription.unsubscribe();
}
}
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,2 +1,17 @@
<span *ngIf="multisig" class="badge badge-pill badge-warning">multisig {{ multisigM }} of {{ multisigN }}</span>
<span *ngIf="secondLayerClose" class="badge badge-pill badge-warning">Layer{{ network === 'liquid' ? '3' : '2' }} Peg-out</span>
<span
*ngIf="multisig"
class="badge badge-pill badge-warning"
i18n="address-labels.multisig"
>multisig {{ multisigM }} of {{ multisigN }}</span>
<span
*ngIf="lightning"
class="badge badge-pill badge-warning"
i18n="address-labels.upper-layer-peg-out"
>Lightning {{ lightning }}</span>
<span
*ngIf="liquid"
class="badge badge-pill badge-warning"
i18n="address-labels.upper-layer-peg-out"
>Liquid {{ liquid }}</span>

View File

@@ -18,7 +18,8 @@ export class AddressLabelsComponent implements OnInit {
multisigM: number;
multisigN: number;
secondLayerClose = false;
lightning = null;
liquid = null;
constructor(
stateService: StateService,
@@ -36,6 +37,29 @@ export class AddressLabelsComponent implements OnInit {
handleVin() {
if (this.vin.inner_witnessscript_asm) {
if (this.vin.inner_witnessscript_asm.indexOf('OP_DEPTH OP_PUSHNUM_12 OP_EQUAL OP_IF OP_PUSHNUM_11') === 0) {
if (this.vin.witness.length > 11) {
this.liquid = 'Peg Out';
} else {
this.liquid = 'Emergency Peg Out';
}
return;
}
[
// {regexp: /^OP_DUP OP_HASH160/, label: 'HTLC'},
{regexp: /^OP_IF OP_PUSHBYTES_33 \w{33} OP_ELSE OP_PUSHBYTES_2 \w{2} OP_CSV OP_DROP/, label: 'Force Close'}
].forEach((item) => {
if (item.regexp.test(this.vin.inner_witnessscript_asm)) {
this.lightning = item.label;
}
}
);
if (this.lightning) {
return;
}
if (this.vin.inner_witnessscript_asm.indexOf('OP_CHECKMULTISIG') > -1) {
const matches = this.getMatches(this.vin.inner_witnessscript_asm, /OP_PUSHNUM_([0-9])/g, 1);
this.multisig = true;
@@ -46,10 +70,6 @@ export class AddressLabelsComponent implements OnInit {
this.multisig = false;
}
}
if (/OP_IF (.+) OP_ELSE (.+) OP_CSV OP_DROP/.test(this.vin.inner_witnessscript_asm)) {
this.secondLayerClose = true;
}
}
if (this.vin.inner_redeemscript_asm && this.vin.inner_redeemscript_asm.indexOf('OP_CHECKMULTISIG') > -1) {

View File

@@ -1,7 +1,7 @@
<div class="container-xl">
<h1 style="float: left;">Address</h1>
<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 : 24 }}</span>
<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>
@@ -16,17 +16,19 @@
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<ng-template [ngIf]="!address.electrum">
<tr>
<td i18n="address.total-received">Total received</td>
<td *ngIf="address.chain_stats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="receieved" [noFiat]="true"></app-amount></td>
</tr>
<tr>
<td i18n="address.total-sent">Total sent</td>
<td *ngIf="address.chain_stats.spent_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="sent" [noFiat]="true"></app-amount></td>
</tr>
</ng-template>
<tr>
<td>Total received</td>
<td><app-amount [satoshis]="receieved" [noFiat]="true"></app-amount></td>
</tr>
<tr>
<td>Total sent</td>
<td><app-amount [satoshis]="sent" [noFiat]="true"></app-amount></td>
</tr>
<tr>
<td>Balance</td>
<td><app-amount [satoshis]="receieved - sent" [noFiat]="true"></app-amount> (<app-fiat [value]="receieved - sent"></app-fiat>)</td>
<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>
</tr>
</tbody>
</table>
@@ -43,7 +45,11 @@
<br>
<h2><ng-template [ngIf]="transactions?.length">{{ (transactions?.length | number) || '?' }} of </ng-template>{{ txCount | number }} transactions</h2>
<h2>
<ng-template [ngIf]="!transactions?.length">&nbsp;</ng-template>
<ng-template i18n="X of X Address Transaction" [ngIf]="transactions?.length === 1">{{ (transactions?.length | number) || '?' }} of {{ txCount | number }} transaction</ng-template>
<ng-template i18n="X of X Address Transactions (Plural)" [ngIf]="transactions?.length > 1">{{ (transactions?.length | number) || '?' }} of {{ txCount | number }} transactions</ng-template>
</h2>
<app-transactions-list [transactions]="transactions" [showConfirmations]="true" (loadMore)="loadMore()"></app-transactions-list>
@@ -63,6 +69,14 @@
</div>
</div>
</div>
<ng-container *ngIf="addressLoadingStatus$ | async as addressLoadingStatus">
<br>
<div class="progress progress-dark">
<div class="progress-bar progress-darklight" role="progressbar" [ngStyle]="{'width': addressLoadingStatus + '%' }"></div>
</div>
</ng-container>
</ng-template>
</div>
@@ -98,12 +112,24 @@
<ng-template [ngIf]="error">
<div class="text-center">
Error loading address data.
<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 view this address on the official Mempool website instead:
<br>
<a href="https://mempool.space/address/{{ addressString }}" target="_blank">https://mempool.space/address/{{ addressString }}</a>
<br>
<a href="http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/address/{{ addressString }}" target="_blank">http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/address/{{ addressString }}</a>
</ng-template>
</div>
</ng-template>
</div>
<br>
<br>
<ng-template #confidentialTd>
<td i18n="shared.confidential">Confidential</td>
</ng-template>

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