Compare commits

..

265 Commits

Author SHA1 Message Date
wiz
90b06833ba Rename frontend npm dep @mempool/mempool-js to @mempool/mempool.js 2021-04-17 12:58:27 +09:00
wiz
221fccf3bc Merge pull request #449 from mempool/simon/syslog-config
Syslog is now configurable with minimum priority and facility option.
2021-04-16 02:54:22 +09:00
wiz
3740980007 Update Exodus logo with new SVG 2021-04-16 02:29:06 +09:00
softsimon
d1b53f4c3a Syslog priority comparison fix. 2021-04-15 21:16:56 +04:00
wiz
d51ea54ab9 Add production FreeBSD tuning parameters to increase network buffers 2021-04-16 00:12:29 +09:00
wiz
3300f0e8d3 Set production backend config "CLEAR_PROTECTION_MINUTES": 5 2021-04-15 21:44:38 +09:00
softsimon
cd1273981d Syslog is now configurable with minimum priority and facility option.
fixes #135
2021-04-15 16:04:09 +04:00
softsimon
fd0ffd2a39 Upgrading mempool-js to v2.3. 2021-04-15 00:57:10 +04:00
wiz
d60bc10941 Update Square Crypto logo with SVG image 2021-04-14 05:04:02 +09:00
wiz
5085e0c420 Add Exodus as Enterprise Sponsor on About page 2021-04-14 04:55:58 +09:00
softsimon
3dbddedf91 Remove 'v' from version. 2021-04-13 18:12:32 +04:00
softsimon
e255bec7ad Updating versions to v2.2.0-dev 2021-04-13 16:53:42 +04:00
softsimon
cbe79d7051 Show backend info at the bottom. Removing frontend hostname. Re-adding frontend version and commit on top. JS error fix. 2021-04-13 11:51:55 +04:00
wiz
089bb38e6a Change log priority for 'The mempool is now in sync!' message to NOTICE 2021-04-13 14:03:36 +09:00
wiz
2077126064 Merge pull request #447 from mempool/simon/frontend-commit-hash-hostname
Backend and frontend commit, version and hostname displayed on the about page
2021-04-13 13:58:59 +09:00
softsimon
7a4ad0ee2f Including gitCommit and version in frontend build. Backend now sending a backendInfo object containing commit, version and hostname. All printed on About page. 2021-04-12 22:17:13 +04:00
wiz
4bb68d0163 Update mempool.space og/twitter preview image with 5 block style logo 2021-04-13 00:13:58 +09:00
wiz
f80a11d1f4 Increase section spacing on About page 2021-04-13 00:10:15 +09:00
wiz
f61e3d8cec Merge pull request #445 from mempool/simon/disk-cache-save-error
Only allow one disk cache saving simultaneously, and allow for partia…
2021-04-12 18:40:32 +09:00
softsimon
7fab42baa5 Only allow one disk cache saving simultaneously, and allow for partially written cache files.
fixes #444
2021-04-12 13:21:49 +04:00
softsimon
4ab4581393 Fix for effective fee rate calculation bug. 2021-04-12 13:02:31 +04:00
wiz
4c8d261da0 Merge pull request #441 from knorrium/fix_address_explorer_typo 2021-04-11 13:34:28 +09:00
Felipe Knorr Kuhn
88c9fd0c7b fix typo on 413/405 error message on the address explorer 2021-04-10 21:04:30 -07:00
softsimon
f1c5f83412 Don't round variables in code, only in templates. 2021-04-10 23:23:05 +04:00
softsimon
2d9b9b5c5d When filtering out lower fee parents, compare with effective fee instead of base fee to include a CPFP chain of transactions. 2021-04-10 21:26:05 +04:00
softsimon
c7c4895eab fixes #438 2021-04-10 20:59:27 +04:00
softsimon
979b870c9c Bump mempool-js version to 2.2.1 2021-04-10 18:58:20 +04:00
softsimon
041aa2a163 Build mempool-js as standalone. 2021-04-10 18:51:13 +04:00
wiz
1bb990b796 Merge pull request #436 from MiguelMedeiros/bugfix/project-contributor-alignment
FIX: align project contributor handle text.
2021-04-10 19:44:26 +09:00
wiz
a6e7e1966e Merge pull request #437 from mempool/simon/contributors-proxy
Proxy contributor requests.
2021-04-10 19:39:26 +09:00
softsimon
db263b8db4 Proxy contributor requests. 2021-04-10 11:33:01 +04:00
Miguel Medeiros
0289620262 FIX: align project contributor handle text. 2021-04-09 17:07:11 -03:00
wiz
d8ef0cd3ac Add BlueWallet as Community Integration on About page
Also try to fix sort order of integrations:
* P2P Exchange Networks
* Raspberry Pi distros
* Onchain wallets
* Lightning wallets
* Watch-only wallets
2021-04-10 02:32:27 +09:00
wiz
68be897379 Fix typo in API documentation for /api/block/:hash/raw method 2021-04-09 21:59:21 +09:00
wiz
2016d4bca6 Merge pull request #433 from mempool/simon/build-mempool-js
Build mempool-js to root www folder.
2021-04-09 12:01:30 +09:00
wiz
0c2d88960c Publish mempool-js output as mempool.js instead of .min.js 2021-04-09 12:01:21 +09:00
softsimon
e3e1cddd2f Build mempool-js to root www folder. 2021-04-09 01:22:24 +04:00
wiz
00b564149d Add Square as Enterprise Sponsor on About page 2021-04-08 00:05:11 +09:00
wiz
dee614f6ac Merge pull request #432 from mempool/simon/angular-11-upgrade
Upgrading all front and backend dependencies, including Angular 11.
2021-04-07 23:31:30 +09:00
wiz
896a4cbcfc Set frontend tsconfig to target es2020 2021-04-07 22:40:13 +09:00
softsimon
e731077077 extractCss is deprecated and always defaults to true. 2021-04-07 17:36:20 +04:00
softsimon
4a907f9dc6 Upgrading all front and backend dependencies, including Angular 11.
fixes #429
2021-04-07 16:18:55 +04:00
wiz
b0baf6aa0d Merge pull request #422 from mempool/simon/corerpcminfee-catcher
Catch getMempoolInfo errors gracefully to not break general main loop
2021-04-06 16:08:20 +09:00
softsimon
8dddfe38a9 Updating getMempoolInfo defaults. 2021-04-06 11:07:38 +04:00
wiz
0f9f905fd1 Merge pull request #419 from mempool/simon/cpfp-duplicate-fix
Fix for duplicate cpfp ancestors.
2021-04-06 15:58:58 +09:00
wiz
bc8e3109fa Merge pull request #427 from mempool/simon/cpfp-ancestors-fee-bug
Bugfix: Ancestors are not able to increase fee of descendants
2021-04-06 15:52:46 +09:00
softsimon
56b0eab9b4 Bugfix: Ancestors are not able to increase fee of descendants
fixes #426
2021-04-06 10:41:13 +04:00
softsimon
5914d99283 Bugfix: Ancestors are not able to increase fee of descendants
fixes #426
2021-04-05 23:45:47 +04:00
softsimon
d942cb48a5 Catch getMempoolInfo errors gracefully to not break general main loop
fixes #411
2021-04-02 11:47:13 +04:00
softsimon
4d0429b786 Fix for duplicate cpfp ancestors.
fixes #414
2021-04-02 00:30:51 +04:00
wiz
2b1c511611 Merge pull request #410 from mempool/wiz/dual-license-under-agplv3-and-gplv3
Dual-license mempool under both the GNU AGPLv3 and GPLv3 licenses
2021-04-02 00:15:59 +09:00
wiz
8a86b63693 Tweak margins and layout spacing on About page 2021-04-01 16:42:37 +09:00
softsimon
2a2c1a6291 Increase space between logos and titles. 2021-03-31 22:49:55 +09:00
wiz
ce1860b7d1 Add Gemini as an Enterprise Sponsor on About page 2021-03-31 22:49:51 +09:00
softsimon
37e01c5e91 Always display one decimal on fee rate for consistency. 2021-03-31 15:43:23 +04:00
wiz
0ff05d5551 Update legal notices for dual-licensed AGPLv3 and GPLv3 on About page 2021-03-28 00:07:03 +09:00
wiz
9b428821f6 Dual-license mempool under both the GNU AGPLv3 and GNU GPLv3 licenses
A few weeks ago in #388, we changed the license for the mempool project
from MIT+CC to AGPLv3. This was generally met with very positive
feedback from the Bitcoin community. However, a few very large
enterprise organizations that love the mempool project are now no longer
able to use our code, since apparently some enterprise organizations
have an internal policy of not using AGPL licensed software.

Since the AGPLv3 license is compatible with the GPLv3 license in various
ways, adding the GPLv3 as an alternative second license for the project
seems like a reasonable way to retain most of the copyleft properties of
our current AGPLv3 license, while also keeping the community happy and
allowing them GPLv3 as another choice if they are prohibited from using
the AGPL internally.

Therefore, I propose to add GPLv3 as an alternative license for the
mempool open source project in this PR. If you are okay with this,
please ACK with "I hereby re-license my contributions to the mempool
open source project under both the GNU AGPLv3 and GPLv3 licenses"

- [ ] @softsimon
- [ ] @wiz
- [ ] @bguillaumat
- [ ] @TechMiX
- [ ] @rbrooklyn
- [ ] @junderw
- [ ] @andrerfneves
- [ ] @andrewtoth
- [ ] @6102bitcoin
- [ ] @Czino
- [ ] @devinbileck
- [ ] @knorrium
- [ ] @jambolo
- [ ] @lucasmoten
- [ ] @fiatjaf
- [ ] @pox
- [ ] @RandyMcMillan
- [ ] @timlucmiptev
2021-03-27 20:02:53 +09:00
wiz
21bb879fc1 Add link to Muun Wallet on About page 2021-03-27 13:12:47 +09:00
wiz
8a97880cdb Merge pull request #408 from knorrium/fix_electrum_dockerfile
fixes to the electrum Dockerfile
2021-03-25 12:45:56 +09:00
Felipe Knorr Kuhn
ca267744a6 fixes to the electrum Dockerfile 2021-03-24 16:24:04 -07:00
wiz
a4253301dc Merge pull request #403 from bitcoincore-dev/makefile
docker: add Makefiles for easier docker deploy
2021-03-24 22:37:18 +09:00
randymcmillan
402c5e3444 deploy: add Makefile for easier docker deploy 2021-03-23 19:57:04 -04:00
softsimon
f12eb333d2 Reuse same i18n string for fee warning tooltip. 2021-03-23 05:18:08 +07:00
wiz
a0b50d7735 Merge pull request #404 from mempool/simon/cpfp-frontend
Visualize the CPFP transactions.
2021-03-22 21:30:00 +09:00
softsimon
3967ce0854 Fix: Compare with tx.feePerVsize to determine if modified fee is the same.
refs #401
2021-03-22 18:24:51 +07:00
softsimon
ed55e86a9d Visualize the CPFP transactions.
fixes #401
2021-03-22 18:04:50 +07:00
softsimon
c93adba276 Merge pull request #402 from mempool/simon/mempool-lazy-delete
Flag transactions for lazy deletion.
2021-03-21 14:58:22 +07:00
softsimon
e05ca7d691 Flag transactions for lazy deletion.
fixes #400
2021-03-21 06:06:03 +07:00
softsimon
e688948e42 Bugfix: Don't extend already extended transactions to not override the firstSeen property.
fixes #390
2021-03-19 19:10:11 +07:00
softsimon
5148de8f17 Shuffle mempool transactions before saving disk cache. (#398)
fixes #397
2021-03-19 13:47:37 +07:00
softsimon
6f1cdd0c8b Adding missing return after expressjs response. 2021-03-18 23:53:39 +07:00
softsimon
d3c53c7406 CPFP support (#395)
* CPFP support.

fixes #5
fixes #353
fixes #360

* Use effectiveFeePerVsize for mempool statistics.

* Renaming endpoint cpfp-info to just cpfp.

* Renaming decended to BestDescendant.

* Updating language file with new strings.
2021-03-18 23:47:40 +07:00
softsimon
b2d08d69cf Round sat/vB in fee rating tooltip.
fixes #364
2021-03-18 19:18:59 +07:00
wiz
b85c2a6e0f Merge pull request #396 from mempool/wiz/add-agpl-logo-to-about-page
Add the GNU AGPLv3 logo to About page
2021-03-18 17:48:32 +09:00
wiz
f1f847a9f8 Add the GNU AGPLv3 logo to About page 2021-03-18 17:44:25 +09:00
wiz
8b5d3dabe7 Update package.json license tags 2021-03-18 16:30:10 +09:00
andrewtoth
ac4588cdab Add recommended fee percentile config (#394) 2021-03-18 12:52:46 +07:00
andrewtoth
baa75b77a7 Fix typo in README (#392) 2021-03-18 12:46:16 +07:00
wiz
f2a08444fe Fix icon for Specter Wallet on About page 2021-03-18 01:14:25 +09:00
wiz
c866fbf6df Add link to Specter Wallet on our About page 2021-03-17 22:23:29 +09:00
wiz
6a07eb0d91 Add link to WARden Portfolio app as Community Integration on About page 2021-03-16 01:56:19 +09:00
wiz
057a96001d Merge pull request #388 from mempool/wiz/change-license-to-agplv3
Change mempool project license to GNU Affero General Public License v3
2021-03-15 06:26:09 +09:00
wiz
f173b17b90 Delete MIT+CC license from Terms of Service, add AGPLv3 to About page 2021-03-15 05:45:53 +09:00
wiz
6b47895aec Change mempool project license to GNU Affero General Public License v3
The files contained in this source code repository are currently
released as "open source" under the terms of the [MIT license](https://opensource.org/licenses/MIT),
with the additional protections of the [Commons Clause License Condition v1](https://commonsclause.com/),
in order to restrict monetization of the mempool app, with the
intended result of mempool remaining free software (as in free
beer) for the Bitcoin community.

However, because of the Commons Clause being added to our license terms,
The Mempool Open Source Project cannot formally be considered as [Free
and open-source software (FOSS)](https://en.wikipedia.org/wiki/Free_and_open-source_software),
according to the generally accepted definition of ["What is free
software?"](https://www.gnu.org/philosophy/free-sw.en.html), and this
has drawn some criticism from the Bitcoin open source community.

Therefore, in order to make The Mempool Open Source Project truly FOSS,
I hereby propose to change our project's license to the [GNU Affero
General Public License v3](https://www.gnu.org/licenses/agpl-3.0.html),
which seems most suitable for a web application. While this won't keep
the mempool free as in free beer, it will keep mempool free as in free
speech, which is what actually matters.

Here is [the TL;DR of AGPLv3](https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)):
> The AGPL license differs from the other GNU licenses in that it was
> built for network software. You can distribute modified versions if
> you keep track of the changes and the date you made them. As per usual
> with GNU licenses, you must license derivatives under AGPL. It
> provides the same restrictions and freedoms as the GPLv3 but with an
> additional clause which makes it so that source code must be
> distributed along with web publication. Since web sites and services
> are never distributed in the traditional sense, the AGPL is the GPL of
> the web.

Could all contributors to The Mempool Open Source Project please ACK
this PR?  @softsimon @wiz @bguillaumat @TechMiX @junderw @andrerfneves
@pox @jambolo @6102bitcoin @devinbileck @fiatjaf @timlucmiptev @Czino
@lucasmoten
2021-03-15 00:59:31 +09:00
Jonathan Underwood
4cbf2e0eb4 Lower volume for sound effects (#385)
* Turn down that racket *shakes cane*

* Maybe a little lower

* Maybe 80%?

* 70?

* 30 ok?

* 65 sounds best
2021-03-12 13:20:40 +07:00
wiz
a530d8b17f Improve grammar, layout, and formatting of Terms of Service page 2021-03-09 14:22:14 +09:00
wiz
e32066affd Display all Project Contributors on About page using GitHub API (#382) 2021-03-09 01:56:41 +07:00
wiz
d5cc558670 Modify nginx.conf to cache HTML for 10m and static resources for 1h
Fixes #61
2021-03-08 18:50:57 +09:00
wiz
a52f98c39d Proxy /api/v1/contributors from mempool.space, also fix HTTP headers
Fixes #366
2021-03-08 18:34:04 +09:00
wiz
7beb832007 Merge pull request #380 from junderw/fixCacheUpdate
Fix bug where legend state is reverted when clicking 1-2 after a 1-minute auto-update.
2021-03-08 16:10:10 +09:00
wiz
3e1c4a7e59 Add link to Bisq's GitHub repo on About page 2021-03-08 12:58:00 +09:00
softsimon
3b8d3221cf Adding enterprise sponsors i18n string. 2021-03-07 21:48:33 +07:00
junderw
b594a9d249 Fix? 2021-03-07 23:45:43 +09:00
softsimon
d20cec4e59 Updating translations from transifex. 2021-03-07 20:13:39 +07:00
softsimon
e5972aa181 Updating transifex. 2021-03-07 20:05:20 +07:00
wiz
b0912064cc Minor tweak of ToS formatting, clarify Bitcoin Only, add 3rd party licenses 2021-03-07 09:37:49 +09:00
wiz
84737bca6e Merge pull request #376 from jambolo/master
Fix mempool-graph legend off-by-one
2021-03-07 03:44:15 +09:00
wiz
14db7e4c8b Update ToS for copyright, mempool.space operator, external link policy (#377)
Update ToS for copyright, mempool.space operator, external link policy
2021-03-05 19:07:31 +07:00
wiz
9624801716 Add a bit more margin to alliances on About page 2021-03-05 16:33:05 +09:00
John Bolton
904cf62c78 Fixes mempool-graph legend off-by-one
Resolves issue #283.
2021-03-04 23:31:09 -08:00
wiz
92e2df4627 Move the Bisq logo on About page from Integrations to Alliances (#375) 2021-03-05 14:30:08 +07:00
wiz
f157a50952 Update hostname in index.html for stats.mempool.space 2021-03-05 15:19:56 +09:00
wiz
c3927c9f0a Fix community integration links on About page to point to GitHub repos 2021-03-05 13:32:52 +09:00
wiz
45505c86d9 Merge pull request #374 from junderw/showFeeTiles
Show fee tiles on mobile
2021-03-05 11:51:07 +09:00
Jonathan Underwood
4fbab41cc8 Show fee tiles on mobile 2021-03-05 11:28:59 +09:00
softsimon
b2769d2af3 Adding Sparrow wallet to about page. 2021-03-05 02:46:23 +07:00
wiz
8fa672e312 Merge pull request #373 from junderw/betterGraph
Graph hides all fee rates below clicked value
2021-03-04 19:34:30 +09:00
Jonathan Underwood
2c5cf94982 Graph hides all fee rates below clicked value 2021-03-04 19:07:20 +09:00
wiz
72ded16543 Tweak padding/margins on About page logos 2021-03-04 16:27:36 +09:00
wiz
7d67c8ea6e Merge pull request #371 from mempool/wiz/add-stuff-to-about-page
Add community integrations / alliances to About page
2021-03-04 16:12:15 +09:00
wiz
1051919a8a Add BLW and Satpile to About page integrations 2021-03-04 16:07:59 +09:00
wiz
8ca6f06650 Fix i18n tags on new About page titles 2021-03-04 15:47:59 +09:00
wiz
906189c43a Add community integrations / alliances to About page, move us to bottom 2021-03-04 12:46:57 +09:00
wiz
0ba6d651c0 Merge pull request #369 from junderw/hex2utf8
Decode hex into utf8 instead of ascii
2021-03-03 21:37:46 +09:00
Jonathan Underwood
2a7b4f9aed Decode hex into utf8 instead of ascii 2021-03-03 21:29:22 +09:00
wiz
892cebd8f4 Enable sending CORS headers from electrs backend in startup script
Fixes #367
2021-03-03 21:29:18 +09:00
wiz
2e8bd7f32e Put the project title back on the About page 2021-03-03 15:48:55 +09:00
wiz
275895bedd Update index.html <head> metadata, OG tags, twitter tags 2021-03-03 11:55:53 +09:00
wiz
ec699f28fb Update link on About page for mempool Twitter account 2021-03-03 11:21:05 +09:00
wiz
85ad0aaa27 Merge pull request #362 from bguillaumat/update-docker
Update docker part
2021-02-27 14:44:52 +09:00
bguillaumat
9e2581d734 Update var from ELECTRS to ELECTRUM for more compatibility 2021-02-26 09:04:14 +01:00
softsimon
be0fd7c582 Credit Hebrew translator 2021-02-25 17:34:21 +07:00
softsimon
26b4bd899b Adding Hebrew language 2021-02-25 17:22:03 +07:00
softsimon
a3d9e87f0e Adding Hebrew language 2021-02-25 17:16:22 +07:00
bguillaumat
4a24a173d3 Update docker README 2021-02-25 10:40:14 +01:00
bguillaumat
05098b1141 Update docker mempool-config 2021-02-25 10:33:04 +01:00
wiz
6a7d5fbe6a Don't start production instances if backend folder doesn't exist 2021-02-25 02:38:38 +09:00
wiz
75b8ec855b Update logo for Liquid network (#359) 2021-02-24 15:16:06 +07:00
wiz
b89630953c Merge pull request #358 from mempool/simon/fee-api-minfee
Send mempoolminfee in fee api.
2021-02-24 16:47:32 +09:00
softsimon
04c42b82f4 Renaming feeApi minimumFee and display as rounded satoshis. 2021-02-24 14:39:34 +07:00
softsimon
6ec07e5315 Re-fixing bug where high priority fee could display as lower than the medium and low fee.
fixes #278
2021-02-24 12:30:40 +07:00
softsimon
ea5dc8738c Send mempoolminfee in fee api.
fixes #357
2021-02-24 12:26:55 +07:00
wiz
619eee9492 Improve production high availability using structured failover and cache 2021-02-23 13:33:52 +09:00
wiz
c9f2bd4029 Update nginx and mempool backend configuration for active failover HA 2021-02-22 20:22:38 +09:00
softsimon
795fb9342a Fix: Double BTC is displayed. 2021-02-21 19:12:50 +07:00
wiz
0b04dbbca2 Merge pull request #352 from mempool/simon/signet-frontend
Signet support in the frontend.
2021-02-21 03:56:46 +09:00
softsimon
55df5dbd35 Signet network color. 2021-02-21 01:45:52 +07:00
wiz
0b79aaaf2f Don't use rust from rustup, use OS package rust instead 2021-02-21 03:44:54 +09:00
wiz
7d858a8abd Modify production startup scripts to support signet 2021-02-21 03:44:33 +09:00
wiz
32af9420dc Update production frontend/backend config to support signet 2021-02-21 03:41:52 +09:00
wiz
7195d6ea31 Update nginx.conf for signet, services, and disable SSR for now 2021-02-21 03:39:19 +09:00
softsimon
a111dc044f Signet should use sBTC not tBTC. 2021-02-21 00:40:47 +07:00
softsimon
563ae8540b Signet support in the frontend. 2021-02-20 23:12:22 +07:00
wiz
279cba5d79 More tweaking of project description on About page 2021-02-18 21:46:16 +09:00
wiz
32c740e572 More tweaking of project description on About page 2021-02-18 21:36:05 +09:00
wiz
28bc08c5a8 Change all usage of <h2> to <h3> on About page 2021-02-18 21:29:15 +09:00
wiz
a0e3468c51 Change 'SNAPSHOT' tag to 'dev' in semver on About page 2021-02-18 21:07:00 +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
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
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
187 changed files with 26642 additions and 11542 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,6 +1,6 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
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

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 }}/

3
.gitignore vendored
View File

@@ -1 +1,4 @@
sitemap
data
docker-compose.yml
backend/mempool-config.json

48
GNUmakefile Executable file
View File

@@ -0,0 +1,48 @@
# If you see pwd_unknown showing up check permissions
PWD ?= pwd_unknown
# DATABASE DEPLOY FOLDER CONFIG - default ./data
ifeq ($(data),)
DATA := data
export DATA
else
DATA := $(data)
export DATA
endif
.PHONY: help
help:
@echo ''
@echo ''
@echo ' Usage: make [COMMAND]'
@echo ''
@echo ' make all # build init mempool and electrs'
@echo ' make init # setup some useful configs'
@echo ' make mempool # build q dockerized mempool.space'
@echo ' make electrs # build a docker electrs image'
@echo ''
.PHONY: init
init:
@echo ''
mkdir -p $(DATA) $(DATA)/mysql $(DATA)/mysql/db-scripts $(DATA)/mysql/data
install -v mariadb-structure.sql $(DATA)/mysql/db-scripts
#REF: https://github.com/mempool/mempool/blob/master/docker/README.md
cat docker/docker-compose.yml > docker-compose.yml
cat backend/mempool-config.sample.json > backend/mempool-config.json
.PHONY: mempool
mempool: init
@echo ''
docker-compose up --force-recreate --always-recreate-deps
@echo ''
.PHONY: electrs
electrum:
#REF: https://hub.docker.com/r/beli/electrum
@echo ''
docker build -f docker/electrum/Dockerfile .
@echo ''
.PHONY: all
all: init
make mempool
#######################
-include Makefile

47
LICENSE
View File

@@ -1,35 +1,24 @@
MIT License with Commons Clause License Condition v1.0
The Mempool Open Source Project
Copyright (c) 2019-2021 The Mempool Open Source Project Developers
Copyright (c) 2019-2020 The Mempool Open Source Project
This program is free software; you can redistribute it and/or modify it under
the terms of (at your option) either:
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, 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:
1) the GNU Affero General Public License as published by the Free Software
Foundation, either version 3 of the License or any later version approved by a
proxy statement published on <https://mempool.space/about>; or
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
2) the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License or any later version approved by a
proxy statement published on <https://mempool.space/about>.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
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.
However, these licenses do not grant you any rights to use the "mempool.space"
trademarks or logos, or any other trademarks of Mempool Space K.K.
Commons Clause License Condition v1.0
This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the full license terms for more details.
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.
You should have received a copy of both the GNU Affero General Public License
and the GNU General Public License along with this program. If not, see
<http://www.gnu.org/licenses/>.

660
LICENSE.AGPL-3.md Normal file
View File

@@ -0,0 +1,660 @@
### GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc.
<https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
### Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains
free software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing
under this license.
The precise terms and conditions for copying, distribution and
modification follow.
### TERMS AND CONDITIONS
#### 0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public
License.
"Copyright" also means copyright-like laws that apply to other kinds
of works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of
an exact copy. The resulting work is called a "modified version" of
the earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user
through a computer network, with no transfer of a copy, is not
conveying.
An interactive user interface displays "Appropriate Legal Notices" to
the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
#### 1. Source Code.
The "source code" for a work means the preferred form of the work for
making modifications to it. "Object code" means any non-source form of
a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users can
regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same
work.
#### 2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey,
without conditions so long as your license otherwise remains in force.
You may convey covered works to others for the sole purpose of having
them make modifications exclusively for you, or provide you with
facilities for running those works, provided that you comply with the
terms of this License in conveying all material for which you do not
control copyright. Those thus making or running the covered works for
you must do so exclusively on your behalf, under your direction and
control, on terms that prohibit them from making any copies of your
copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the
conditions stated below. Sublicensing is not allowed; section 10 makes
it unnecessary.
#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such
circumvention is effected by exercising rights under this License with
respect to the covered work, and you disclaim any intention to limit
operation or modification of the work as a means of enforcing, against
the work's users, your or third parties' legal rights to forbid
circumvention of technological measures.
#### 4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
#### 5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these
conditions:
- a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
- b) The work must carry prominent notices stating that it is
released under this License and any conditions added under
section 7. This requirement modifies the requirement in section 4
to "keep intact all notices".
- c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
- d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
#### 6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of
sections 4 and 5, provided that you also convey the machine-readable
Corresponding Source under the terms of this License, in one of these
ways:
- a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
- b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the Corresponding
Source from a network server at no charge.
- c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
- d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
- e) Convey the object code using peer-to-peer transmission,
provided you inform other peers where the object code and
Corresponding Source of the work are being offered to the general
public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal,
family, or household purposes, or (2) anything designed or sold for
incorporation into a dwelling. In determining whether a product is a
consumer product, doubtful cases shall be resolved in favor of
coverage. For a particular product received by a particular user,
"normally used" refers to a typical or common use of that class of
product, regardless of the status of the particular user or of the way
in which the particular user actually uses, or expects or is expected
to use, the product. A product is a consumer product regardless of
whether the product has substantial commercial, industrial or
non-consumer uses, unless such uses represent the only significant
mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to
install and execute modified versions of a covered work in that User
Product from a modified version of its Corresponding Source. The
information must suffice to ensure that the continued functioning of
the modified object code is in no case prevented or interfered with
solely because modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or
updates for a work that has been modified or installed by the
recipient, or for the User Product in which it has been modified or
installed. Access to a network may be denied when the modification
itself materially and adversely affects the operation of the network
or violates the rules and protocols for communication across the
network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
#### 7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders
of that material) supplement the terms of this License with terms:
- a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
- b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
- c) Prohibiting misrepresentation of the origin of that material,
or requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
- d) Limiting the use for publicity purposes of names of licensors
or authors of the material; or
- e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
- f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions
of it) with contractual assumptions of liability to the recipient,
for any liability that these contractual assumptions directly
impose on those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions; the
above requirements apply either way.
#### 8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your license
from a particular copyright holder is reinstated (a) provisionally,
unless and until the copyright holder explicitly and finally
terminates your license, and (b) permanently, if the copyright holder
fails to notify you of the violation by some reasonable means prior to
60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
#### 9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run
a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
#### 10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
#### 11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims owned
or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within the
scope of its coverage, prohibits the exercise of, or is conditioned on
the non-exercise of one or more of the rights that are specifically
granted under this License. You may not convey a covered work if you
are a party to an arrangement with a third party that is in the
business of distributing software, under which you make payment to the
third party based on the extent of your activity of conveying the
work, and under which the third party grants, to any of the parties
who would receive the covered work from you, a discriminatory patent
license (a) in connection with copies of the covered work conveyed by
you (or copies made from those copies), or (b) primarily for and in
connection with specific products or compilations that contain the
covered work, unless you entered into that arrangement, or that patent
license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
#### 12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under
this License and any other pertinent obligations, then as a
consequence you may not convey it at all. For example, if you agree to
terms that obligate you to collect a royalty for further conveying
from those to whom you convey the Program, the only way you could
satisfy both those terms and this License would be to refrain entirely
from conveying the Program.
#### 13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your
version supports such interaction) an opportunity to receive the
Corresponding Source of your version by providing access to the
Corresponding Source from a network server at no charge, through some
standard or customary means of facilitating copying of software. This
Corresponding Source shall include the Corresponding Source for any
work covered by version 3 of the GNU General Public License that is
incorporated pursuant to the following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
#### 14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Affero General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever
published by the Free Software Foundation.
If the Program specifies that a proxy can decide which future versions
of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
#### 15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
CORRECTION.
#### 16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
#### 17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
### How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these
terms.
To do so, attach the following notices to the program. It is safest to
attach them to the start of each source file to most effectively state
the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper
mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for
the specific requirements.
You should also get your employer (if you work as a programmer) or
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. For more information on this, and how to apply and follow
the GNU AGPL, see <https://www.gnu.org/licenses/>.

675
LICENSE.GPL-3.md Normal file
View File

@@ -0,0 +1,675 @@
### GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
<https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
### Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom
to share and change all versions of a program--to make sure it remains
free software for all its users. We, the Free Software Foundation, use
the GNU General Public License for most of our software; it applies
also to any other work released this way by its authors. You can apply
it to your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you
have certain responsibilities if you distribute copies of the
software, or if you modify it: responsibilities to respect the freedom
of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the
manufacturer can do so. This is fundamentally incompatible with the
aim of protecting users' freedom to change the software. The
systematic pattern of such abuse occurs in the area of products for
individuals to use, which is precisely where it is most unacceptable.
Therefore, we have designed this version of the GPL to prohibit the
practice for those products. If such problems arise substantially in
other domains, we stand ready to extend this provision to those
domains in future versions of the GPL, as needed to protect the
freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish
to avoid the special danger that patents applied to a free program
could make it effectively proprietary. To prevent this, the GPL
assures that patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
### TERMS AND CONDITIONS
#### 0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds
of works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of
an exact copy. The resulting work is called a "modified version" of
the earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user
through a computer network, with no transfer of a copy, is not
conveying.
An interactive user interface displays "Appropriate Legal Notices" to
the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
#### 1. Source Code.
The "source code" for a work means the preferred form of the work for
making modifications to it. "Object code" means any non-source form of
a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users can
regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same
work.
#### 2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey,
without conditions so long as your license otherwise remains in force.
You may convey covered works to others for the sole purpose of having
them make modifications exclusively for you, or provide you with
facilities for running those works, provided that you comply with the
terms of this License in conveying all material for which you do not
control copyright. Those thus making or running the covered works for
you must do so exclusively on your behalf, under your direction and
control, on terms that prohibit them from making any copies of your
copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the
conditions stated below. Sublicensing is not allowed; section 10 makes
it unnecessary.
#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such
circumvention is effected by exercising rights under this License with
respect to the covered work, and you disclaim any intention to limit
operation or modification of the work as a means of enforcing, against
the work's users, your or third parties' legal rights to forbid
circumvention of technological measures.
#### 4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
#### 5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these
conditions:
- a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
- b) The work must carry prominent notices stating that it is
released under this License and any conditions added under
section 7. This requirement modifies the requirement in section 4
to "keep intact all notices".
- c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
- d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
#### 6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of
sections 4 and 5, provided that you also convey the machine-readable
Corresponding Source under the terms of this License, in one of these
ways:
- a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
- b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the Corresponding
Source from a network server at no charge.
- c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
- d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
- e) Convey the object code using peer-to-peer transmission,
provided you inform other peers where the object code and
Corresponding Source of the work are being offered to the general
public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal,
family, or household purposes, or (2) anything designed or sold for
incorporation into a dwelling. In determining whether a product is a
consumer product, doubtful cases shall be resolved in favor of
coverage. For a particular product received by a particular user,
"normally used" refers to a typical or common use of that class of
product, regardless of the status of the particular user or of the way
in which the particular user actually uses, or expects or is expected
to use, the product. A product is a consumer product regardless of
whether the product has substantial commercial, industrial or
non-consumer uses, unless such uses represent the only significant
mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to
install and execute modified versions of a covered work in that User
Product from a modified version of its Corresponding Source. The
information must suffice to ensure that the continued functioning of
the modified object code is in no case prevented or interfered with
solely because modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or
updates for a work that has been modified or installed by the
recipient, or for the User Product in which it has been modified or
installed. Access to a network may be denied when the modification
itself materially and adversely affects the operation of the network
or violates the rules and protocols for communication across the
network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
#### 7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders
of that material) supplement the terms of this License with terms:
- a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
- b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
- c) Prohibiting misrepresentation of the origin of that material,
or requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
- d) Limiting the use for publicity purposes of names of licensors
or authors of the material; or
- e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
- f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions
of it) with contractual assumptions of liability to the recipient,
for any liability that these contractual assumptions directly
impose on those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions; the
above requirements apply either way.
#### 8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your license
from a particular copyright holder is reinstated (a) provisionally,
unless and until the copyright holder explicitly and finally
terminates your license, and (b) permanently, if the copyright holder
fails to notify you of the violation by some reasonable means prior to
60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
#### 9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run
a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
#### 10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
#### 11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims owned
or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within the
scope of its coverage, prohibits the exercise of, or is conditioned on
the non-exercise of one or more of the rights that are specifically
granted under this License. You may not convey a covered work if you
are a party to an arrangement with a third party that is in the
business of distributing software, under which you make payment to the
third party based on the extent of your activity of conveying the
work, and under which the third party grants, to any of the parties
who would receive the covered work from you, a discriminatory patent
license (a) in connection with copies of the covered work conveyed by
you (or copies made from those copies), or (b) primarily for and in
connection with specific products or compilations that contain the
covered work, unless you entered into that arrangement, or that patent
license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
#### 12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under
this License and any other pertinent obligations, then as a
consequence you may not convey it at all. For example, if you agree to
terms that obligate you to collect a royalty for further conveying
from those to whom you convey the Program, the only way you could
satisfy both those terms and this License would be to refrain entirely
from conveying the Program.
#### 13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
#### 14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions
of the GNU General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in
detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies that a certain numbered version of the GNU General Public
License "or any later version" applies to it, you have the option of
following the terms and conditions either of that numbered version or
of any later version published by the Free Software Foundation. If the
Program does not specify a version number of the GNU General Public
License, you may choose any version ever published by the Free
Software Foundation.
If the Program specifies that a proxy can decide which future versions
of the GNU General Public License can be used, that proxy's public
statement of acceptance of a version permanently authorizes you to
choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
#### 15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
CORRECTION.
#### 16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
#### 17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
### How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these
terms.
To do so, attach the following notices to the program. It is safest to
attach them to the start of each source file to most effectively state
the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper
mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands \`show w' and \`show c' should show the
appropriate parts of the General Public License. Of course, your
program's commands might be different; for a GUI interface, you would
use an "about box".
You should also get your employer (if you work as a programmer) or
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. For more information on this, and how to apply and follow
the GNU GPL, see <https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your
program into proprietary programs. If your program is a subroutine
library, you may consider it more useful to permit linking proprietary
applications with the library. If this is what you want to do, use the
GNU Lesser General Public License instead of this License. But first,
please read <https://www.gnu.org/licenses/why-not-lgpl.html>.

1
Makefile Normal file
View File

@@ -0,0 +1 @@
# For additional configs/scripting

View File

@@ -1,10 +1,22 @@
# The Mempool Open Source Project
Mempool is the fully featured mempool visualizer and block explorer website and API service running on [mempool.space](https://mempool.space/). The instructions below are for most users at home running on low-powered Raspberry Pi devices, but if you want to run a production website on a powerful server, see the [production setup guide](https://github.com/mempool/mempool/tree/master/production)
Mempool is the fully featured visualizer, explorer, and API service running on [mempool.space](https://mempool.space/), an open source project developed and operated for the benefit of the Bitcoin community, with a focus on the emerging transaction fee market to help our transition into a multi-layer ecosystem.
![mempool](https://pbs.twimg.com/media/Ei8p_flUcAEjfXE?format=jpg&name=4096x4096)
![mempool](https://mempool.space/resources/screenshots/v2.1.0-dashboard.png)
# Installation
## Installation Methods
Mempool can be self-hosted on a wide variety of your own hardware, ranging from a simple one-click installation on a Raspberry Pi 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:
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)
# Manual Installation
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
@@ -53,7 +65,7 @@ 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)
```
@@ -67,7 +79,7 @@ Install mempool dependencies from npm and build the backend:
```bash
# backend
cd ../backend/
cd backend
npm install
npm run build
```
@@ -96,11 +108,10 @@ Edit `mempool-config.json` to add your Bitcoin Core node RPC credentials:
"HOST": "127.0.0.1",
"PORT": 50002,
"TLS_ENABLED": true,
"TX_LOOKUPS": false
},
"DATABASE": {
"ENABLED": true,
"HOST": "localhost",
"HOST": "127.0.0.1",
"PORT": 3306,
"USERNAME": "mempool",
"PASSWORD": "mempool",

3
backend/.gitignore vendored
View File

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

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

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

View File

@@ -5,7 +5,10 @@
"HTTP_PORT": 8999,
"SPAWN_CLUSTER_PROCS": 0,
"API_URL_PREFIX": "/api/v1/",
"POLL_RATE_MS": 2000
"POLL_RATE_MS": 2000,
"CACHE_DIR": "./cache",
"CLEAR_PROTECTION_MINUTES": 20,
"RECOMMENDED_FEE_PERCENTILE": 50
},
"CORE_RPC": {
"HOST": "127.0.0.1",
@@ -16,12 +19,18 @@
"ELECTRUM": {
"HOST": "127.0.0.1",
"PORT": 50002,
"TLS_ENABLED": true,
"TX_LOOKUPS": false
"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",
@@ -30,6 +39,13 @@
"USERNAME": "mempool",
"PASSWORD": "mempool"
},
"SYSLOG": {
"ENABLED": true,
"HOST": "127.0.0.1",
"PORT": 514,
"MIN_PRIORITY": "info",
"FACILITY": "local7"
},
"STATISTICS": {
"ENABLED": true,
"TX_PER_SECOND_SAMPLE_PERIOD": 150
@@ -41,12 +57,5 @@
"BISQ_MARKETS": {
"ENABLED": false,
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
},
"SPONSORS": {
"ENABLED": false,
"BTCPAY_URL": "",
"BTCPAY_AUTH": "",
"BTCPAY_WEBHOOK_URL": "",
"TWITTER_BEARER_AUTH": ""
}
}

View File

@@ -7,7 +7,7 @@
"": {
"name": "mempool-backend",
"version": "2.0.0",
"license": "MIT",
"license": "GNU Affero General Public License v3.0",
"dependencies": {
"@mempool/bitcoin": "^3.0.2",
"@mempool/electrum-client": "^1.1.7",
@@ -16,17 +16,17 @@
"crypto-js": "^4.0.0",
"express": "^4.17.1",
"locutus": "^2.0.12",
"mysql2": "^1.6.1",
"node-worker-threads-pool": "^1.4.2",
"ws": "^7.3.1"
"mysql2": "2.2.5",
"node-worker-threads-pool": "^1.4.3",
"ws": "^7.4.4"
},
"devDependencies": {
"@types/compression": "^1.0.1",
"@types/express": "^4.17.2",
"@types/locutus": "^0.0.6",
"@types/ws": "^6.0.4",
"tslint": "~6.1.0",
"typescript": "~3.9.7"
"tslint": "^6.1.0",
"typescript": "^4.1.5"
}
},
"node_modules/@babel/code-frame": {
@@ -911,11 +911,14 @@
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dependencies": {
"yallist": "^3.0.2"
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/md5.js": {
@@ -1030,29 +1033,29 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/mysql2": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-1.7.0.tgz",
"integrity": "sha512-xTWWQPjP5rcrceZQ7CSTKR/4XIDeH/cRkNH/uzvVGQ7W5c7EJ0dXeJUusk7OKhIoHj7uFKUxDVSCfLIl+jluog==",
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.2.5.tgz",
"integrity": "sha512-XRqPNxcZTpmFdXbJqb+/CtYVLCx14x1RTeNMD4954L331APu75IC74GDqnZMEt1kwaXy6TySo55rF2F3YJS78g==",
"dependencies": {
"denque": "^1.4.1",
"generate-function": "^2.3.1",
"iconv-lite": "^0.5.0",
"iconv-lite": "^0.6.2",
"long": "^4.0.0",
"lru-cache": "^5.1.1",
"lru-cache": "^6.0.0",
"named-placeholders": "^1.1.2",
"seq-queue": "^0.0.5",
"sqlstring": "^2.3.1"
"sqlstring": "^2.3.2"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/mysql2/node_modules/iconv-lite": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz",
"integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==",
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
"integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
@@ -1477,9 +1480,9 @@
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
},
"node_modules/typescript": {
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
"integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz",
"integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@@ -1541,9 +1544,9 @@
"dev": true
},
"node_modules/ws": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz",
"integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==",
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
"engines": {
"node": ">=8.3.0"
},
@@ -1561,9 +1564,9 @@
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
},
"dependencies": {
@@ -2331,11 +2334,11 @@
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
},
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"requires": {
"yallist": "^3.0.2"
"yallist": "^4.0.0"
}
},
"md5.js": {
@@ -2426,26 +2429,26 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"mysql2": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-1.7.0.tgz",
"integrity": "sha512-xTWWQPjP5rcrceZQ7CSTKR/4XIDeH/cRkNH/uzvVGQ7W5c7EJ0dXeJUusk7OKhIoHj7uFKUxDVSCfLIl+jluog==",
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.2.5.tgz",
"integrity": "sha512-XRqPNxcZTpmFdXbJqb+/CtYVLCx14x1RTeNMD4954L331APu75IC74GDqnZMEt1kwaXy6TySo55rF2F3YJS78g==",
"requires": {
"denque": "^1.4.1",
"generate-function": "^2.3.1",
"iconv-lite": "^0.5.0",
"iconv-lite": "^0.6.2",
"long": "^4.0.0",
"lru-cache": "^5.1.1",
"lru-cache": "^6.0.0",
"named-placeholders": "^1.1.2",
"seq-queue": "^0.0.5",
"sqlstring": "^2.3.1"
"sqlstring": "^2.3.2"
},
"dependencies": {
"iconv-lite": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz",
"integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==",
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
"integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
}
}
@@ -2782,9 +2785,9 @@
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
},
"typescript": {
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
"integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz",
"integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==",
"dev": true
},
"unpipe": {
@@ -2830,15 +2833,15 @@
"dev": true
},
"ws": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz",
"integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==",
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
"requires": {}
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
}
}

View File

@@ -1,8 +1,8 @@
{
"name": "mempool-backend",
"version": "2.0.1",
"version": "2.2.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "MIT",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",
"repository": {
"type": "git",
@@ -23,7 +23,8 @@
"ng": "./node_modules/@angular/cli/bin/ng",
"tsc": "./node_modules/typescript/bin/tsc",
"build": "npm run tsc",
"start": "node --max-old-space-size=4096 dist/index.js",
"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": {
@@ -34,16 +35,16 @@
"crypto-js": "^4.0.0",
"express": "^4.17.1",
"locutus": "^2.0.12",
"mysql2": "^1.6.1",
"node-worker-threads-pool": "^1.4.2",
"ws": "^7.3.1"
"mysql2": "2.2.5",
"node-worker-threads-pool": "^1.4.3",
"ws": "^7.4.4"
},
"devDependencies": {
"@types/compression": "^1.0.1",
"@types/express": "^4.17.2",
"@types/locutus": "^0.0.6",
"@types/ws": "^6.0.4",
"tslint": "~6.1.0",
"typescript": "~3.9.7"
"tslint": "^6.1.0",
"typescript": "^4.1.5"
}
}

View File

@@ -1,20 +1,24 @@
import * as fs from 'fs';
import * as os from 'os';
import logger from '../logger';
import { IBackendInfo } from '../mempool.interfaces';
class BackendInfo {
gitCommitHash = '';
hostname = '';
private gitCommitHash = '';
private hostname = '';
private version = '';
constructor() {
this.setLatestCommitHash();
this.setVersion();
this.hostname = os.hostname();
}
public getBackendInfo() {
public getBackendInfo(): IBackendInfo {
return {
'hostname': this.hostname,
'git-commit': this.gitCommitHash,
hostname: this.hostname,
gitCommit: this.gitCommitHash,
version: this.version,
};
}
@@ -29,6 +33,15 @@ class BackendInfo {
logger.err('Could not load git commit info: ' + e.message || e);
}
}
private setVersion(): void {
try {
const packageJson = fs.readFileSync('package.json').toString();
this.version = JSON.parse(packageJson).version;
} catch (e) {
throw new Error(e);
}
}
}
export default new BackendInfo();

View File

@@ -43,7 +43,7 @@ class Bisq {
}
handleNewBitcoinBlock(block: BlockExtended): void {
if (block.height - 2 > this.latestBlockHeight && this.latestBlockHeight !== 0) {
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();
@@ -138,7 +138,7 @@ class Bisq {
}
private updatePrice() {
axios.get<BisqTrade[]>('https://bisq.markets/api/trades/?market=bsq_btc')
axios.get<BisqTrade[]>('https://bisq.markets/api/trades/?market=bsq_btc', { timeout: 10000 })
.then((response) => {
const prices: number[] = [];
response.data.forEach((trade) => {
@@ -233,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,
};

View File

@@ -3,7 +3,6 @@ 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>;

View File

@@ -5,7 +5,6 @@ import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { IBitcoinApi } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface';
import blocks from '../blocks';
import bitcoinBaseApi from './bitcoin-base.api';
import mempool from '../mempool';
import { TransactionExtended } from '../../mempool.interfaces';
@@ -23,16 +22,6 @@ class BitcoinApi implements AbstractBitcoinApi {
});
}
$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];
@@ -48,6 +37,9 @@ class BitcoinApi implements AbstractBitcoinApi {
return this.bitcoindClient.getRawTransaction(txId, true)
.then((transaction: IBitcoinApi.Transaction) => {
if (skipConversion) {
transaction.vout.forEach((vout) => {
vout.value = vout.value * 100000000;
});
return transaction;
}
return this.$convertTransaction(transaction, addPrevout);
@@ -202,12 +194,12 @@ class BitcoinApi implements AbstractBitcoinApi {
}
let mempoolEntry: IBitcoinApi.MempoolEntry;
if (!mempool.isInSync() && !this.rawMempoolCache) {
this.rawMempoolCache = await bitcoinBaseApi.$getRawMempoolVerbose();
this.rawMempoolCache = await this.$getRawMempoolVerbose();
}
if (this.rawMempoolCache && this.rawMempoolCache[transaction.txid]) {
mempoolEntry = this.rawMempoolCache[transaction.txid];
} else {
mempoolEntry = await bitcoinBaseApi.$getMempoolEntry(transaction.txid);
mempoolEntry = await this.$getMempoolEntry(transaction.txid);
}
transaction.fee = mempoolEntry.fees.base * 100000000;
return transaction;
@@ -238,6 +230,14 @@ class BitcoinApi implements AbstractBitcoinApi {
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;
@@ -285,6 +285,10 @@ class BitcoinApi implements AbstractBitcoinApi {
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) {

View File

@@ -4,6 +4,7 @@ import { IBitcoinApi } from './bitcoin-api.interface';
class BitcoinBaseApi {
bitcoindClient: any;
bitcoindClientMempoolInfo: any;
constructor() {
this.bitcoindClient = new bitcoin.Client({
@@ -13,24 +14,32 @@ class BitcoinBaseApi {
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();
}
$getRawTransaction(txId: string): Promise<IBitcoinApi.Transaction> {
return this.bitcoindClient.getRawTransaction(txId, true);
}
$getMempoolEntry(txid: string): Promise<IBitcoinApi.MempoolEntry> {
return this.bitcoindClient.getMempoolEntry(txid);
}
$getRawMempoolVerbose(): Promise<IBitcoinApi.RawMempool> {
return this.bitcoindClient.getRawMemPool(true);
}
}
export default new BitcoinBaseApi();

View File

@@ -43,25 +43,6 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
});
}
async $getRawTransaction(txId: string, skipConversion = false, addPrevout = false): Promise<IEsploraApi.Transaction> {
if (!config.ELECTRUM.TX_LOOKUPS) {
return super.$getRawTransaction(txId, skipConversion, addPrevout);
}
const txInMempool = mempool.getMempool()[txId];
if (txInMempool && addPrevout) {
return this.$addPrevouts(txInMempool);
}
const transaction: IBitcoinApi.Transaction = await this.electrumClient.blockchainTransaction_get(txId, true);
if (!transaction) {
throw new Error('Unable to get transaction: ' + txId);
}
if (skipConversion) {
// @ts-ignore
return transaction;
}
return this.$convertTransaction(transaction, addPrevout);
}
async $getAddress(address: string): Promise<IEsploraApi.Address> {
const addressInfo = await this.$validateAddress(address);
if (!addressInfo || !addressInfo.isvalid) {
@@ -105,7 +86,8 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
'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') {

View File

@@ -91,6 +91,7 @@ export namespace IEsploraApi {
address: string;
chain_stats: ChainStats;
mempool_stats: MempoolStats;
electrum?: boolean;
}
export interface ChainStats {

View File

@@ -1,39 +1,42 @@
import config from '../../config';
import axios from 'axios';
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')
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)
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')
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')
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)
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)
return axios.get<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash, this.axiosConfig)
.then((response) => response.data);
}
@@ -45,11 +48,6 @@ class ElectrsApi implements AbstractBitcoinApi {
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)
.then((response) => response.data);
}
$getAddressPrefix(prefix: string): string[] {
throw new Error('Method not implemented.');
}

View File

@@ -67,42 +67,38 @@ class Blocks {
let transactionsFound = 0;
for (let i = 0; i < txIds.length; i++) {
// When using bitcoind, just fetch the coinbase tx for now
if (config.MEMPOOL.BACKEND !== 'esplora' && i === 0) {
let txFound = false;
let findCoinbaseTxTries = 0;
// It takes Electrum Server a few seconds to index the transaction after a block is found
while (findCoinbaseTxTries < 5 && !txFound) {
const tx = await transactionUtils.$getTransactionExtended(txIds[i]);
if (tx) {
txFound = true;
transactions.push(tx);
} else {
await Common.sleep(1000);
findCoinbaseTxTries++;
}
}
}
if (mempool[txIds[i]]) {
transactions.push(mempool[txIds[i]]);
transactionsFound++;
} else if (config.MEMPOOL.BACKEND === 'esplora') {
} else if (config.MEMPOOL.BACKEND === 'esplora' || memPool.isInSync() || i === 0) {
logger.debug(`Fetching block tx ${i} of ${txIds.length}`);
const tx = await transactionUtils.$getTransactionExtended(txIds[i]);
if (tx) {
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]);
}
}
}
}
transactions.forEach((tx) => {
if (!tx.cpfpChecked) {
Common.setRelativesAndGetCpfpInfo(tx, mempool);
}
});
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];
transactions.shift();
transactions.sort((a, b) => b.effectiveFeePerVsize - a.effectiveFeePerVsize);
blockExtended.medianFee = transactions.length > 1 ? Common.median(transactions.map((tx) => tx.effectiveFeePerVsize)) : 0;
blockExtended.feeRange = transactions.length > 1 ? Common.getFeesInRange(transactions, 8) : [0, 0];
if (block.height % 2016 === 0) {
this.lastDifficultyAdjustmentTime = block.timestamp;
@@ -116,7 +112,9 @@ class Blocks {
if (this.newBlockCallbacks.length) {
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
}
diskCache.$saveCacheToDisk();
if (memPool.isInSync()) {
diskCache.$saveCacheToDisk();
}
}
}

View File

@@ -1,4 +1,4 @@
import { TransactionExtended, TransactionStripped } from '../mempool.interfaces';
import { CpfpInfo, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
export class Common {
static median(numbers: number[]) {
@@ -12,17 +12,28 @@ export class Common {
return medianNr;
}
static percentile(numbers: number[], percentile: number) {
if (percentile === 50) {
return this.median(numbers);
}
const index = Math.ceil(numbers.length * (100 - percentile) * 1e-2);
if (index < 0 || index > numbers.length - 1) {
return 0;
}
return numbers[index];
}
static getFeesInRange(transactions: TransactionExtended[], rangeLength: number) {
const arr = [transactions[transactions.length - 1].feePerVsize];
const arr = [transactions[transactions.length - 1].effectiveFeePerVsize];
const chunk = 1 / (rangeLength - 1);
let itemsToAdd = rangeLength - 2;
while (itemsToAdd > 0) {
arr.push(transactions[Math.floor(transactions.length * chunk * itemsToAdd)].feePerVsize);
arr.push(transactions[Math.floor(transactions.length * chunk * itemsToAdd)].effectiveFeePerVsize);
itemsToAdd--;
}
arr.push(transactions[0].feePerVsize);
arr.push(transactions[0].effectiveFeePerVsize);
return arr;
}
@@ -52,7 +63,7 @@ export class Common {
return {
txid: tx.txid,
fee: tx.fee,
weight: tx.weight,
vsize: tx.weight / 4,
value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
};
}
@@ -64,4 +75,74 @@ export class Common {
}, ms);
});
}
static shuffleArray(array: any[]) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
static setRelativesAndGetCpfpInfo(tx: TransactionExtended, memPool: { [txid: string]: TransactionExtended }): CpfpInfo {
const parents = this.findAllParents(tx, memPool);
const lowerFeeParents = parents.filter((parent) => parent.feePerVsize < tx.effectiveFeePerVsize);
let totalWeight = tx.weight + lowerFeeParents.reduce((prev, val) => prev + val.weight, 0);
let totalFees = tx.fee + lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
tx.ancestors = parents
.map((t) => {
return {
txid: t.txid,
weight: t.weight,
fee: t.fee,
};
});
// Add high (high fee) decendant weight and fees
if (tx.bestDescendant) {
totalWeight += tx.bestDescendant.weight;
totalFees += tx.bestDescendant.fee;
}
tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
tx.cpfpChecked = true;
return {
ancestors: tx.ancestors,
bestDescendant: tx.bestDescendant || null,
};
}
private static findAllParents(tx: TransactionExtended, memPool: { [txid: string]: TransactionExtended }): TransactionExtended[] {
let parents: TransactionExtended[] = [];
tx.vin.forEach((parent) => {
if (parents.find((p) => p.txid === parent.txid)) {
return;
}
const parentTx = memPool[parent.txid];
if (parentTx) {
if (tx.bestDescendant && tx.bestDescendant.fee / (tx.bestDescendant.weight / 4) > parentTx.feePerVsize) {
if (parentTx.bestDescendant && parentTx.bestDescendant.fee < tx.fee + tx.bestDescendant.fee) {
parentTx.bestDescendant = {
weight: tx.weight + tx.bestDescendant.weight,
fee: tx.fee + tx.bestDescendant.fee,
txid: tx.txid,
};
}
} else if (tx.feePerVsize > parentTx.feePerVsize) {
parentTx.bestDescendant = {
weight: tx.weight,
fee: tx.fee,
txid: tx.txid
};
}
parents.push(parentTx);
parents = parents.concat(this.findAllParents(parentTx, memPool));
}
});
return parents;
}
}

View File

@@ -1,65 +1,59 @@
import * as fs from 'fs';
const fsPromises = fs.promises;
import * as process from 'process';
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';
import { Common } from './common';
class DiskCache {
private static FILE_NAME = './cache.json';
private static FILE_NAME_2 = './cache2.json';
private static CHUNK_SIZE = 50000;
constructor() {
if (!cluster.isMaster) {
return;
}
process.on('SIGINT', () => {
this.saveCacheToDiskSync();
process.exit(2);
});
process.on('SIGTERM', () => {
this.saveCacheToDiskSync();
process.exit(2);
});
}
private static FILE_NAME = config.MEMPOOL.CACHE_DIR + '/cache.json';
private static FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/cache{number}.json';
private static CHUNK_FILES = 25;
private isWritingCache = false;
constructor() { }
async $saveCacheToDisk(): Promise<void> {
if (!cluster.isMaster) {
return;
}
if (this.isWritingCache) {
logger.debug('Saving cache already in progress. Skipping.')
return;
}
try {
logger.debug('Writing mempool and blocks data to disk cache (async)...');
const mempoolChunk_1 = Object.fromEntries(Object.entries(memPool.getMempool()).splice(0, DiskCache.CHUNK_SIZE));
const mempoolChunk_2 = Object.fromEntries(Object.entries(memPool.getMempool()).splice(DiskCache.CHUNK_SIZE));
this.isWritingCache = true;
const mempool = memPool.getMempool();
const mempoolArray: TransactionExtended[] = [];
for (const tx in mempool) {
mempoolArray.push(mempool[tx]);
}
Common.shuffleArray(mempoolArray);
const chunkSize = Math.floor(mempoolArray.length / DiskCache.CHUNK_FILES);
await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({
blocks: blocks.getBlocks(),
mempool: mempoolChunk_1
}), {flag: 'w'});
await fsPromises.writeFile(DiskCache.FILE_NAME_2, JSON.stringify({
mempool: mempoolChunk_2
}), {flag: 'w'});
logger.debug('Mempool and blocks data saved to disk cache');
} catch (e) {
logger.warn('Error writing to cache file: ' + e.message || e);
}
}
saveCacheToDiskSync(): void {
try {
logger.debug('Writing mempool and blocks data to disk cache...');
const mempoolChunk_1 = Object.fromEntries(Object.entries(memPool.getMempool()).splice(0, DiskCache.CHUNK_SIZE));
const mempoolChunk_2 = Object.fromEntries(Object.entries(memPool.getMempool()).splice(DiskCache.CHUNK_SIZE));
fs.writeFileSync(DiskCache.FILE_NAME, JSON.stringify({
blocks: blocks.getBlocks(),
mempool: mempoolChunk_1
}), {flag: 'w'});
fs.writeFileSync(DiskCache.FILE_NAME_2, JSON.stringify({
mempool: mempoolChunk_2
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');
this.isWritingCache = false;
} catch (e) {
logger.warn('Error writing to cache file: ' + e.message || e);
this.isWritingCache = false;
}
}
@@ -67,20 +61,42 @@ class DiskCache {
if (!fs.existsSync(DiskCache.FILE_NAME)) {
return;
}
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);
}
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;
}
}
}
if (fs.existsSync(DiskCache.FILE_NAME_2)) {
const cacheData2 = JSON.parse(fs.readFileSync(DiskCache.FILE_NAME_2, 'utf8'));
Object.assign(data.mempool, cacheData2.mempool);
}
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
const fileName = DiskCache.FILE_NAMES.replace('{number}', i.toString());
try {
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);
}
}
} catch (e) {
logger.debug('Error parsing ' + fileName + '. Skipping.');
}
}
memPool.setMempool(data.mempool);
blocks.setBlocks(data.blocks);
memPool.setMempool(data.mempool);
blocks.setBlocks(data.blocks);
} catch (e) {
logger.warn('Failed to parse mempoool and blocks cache. Skipping.');
}
}
}

View File

@@ -1,201 +0,0 @@
import config from '../config';
import axios from 'axios';
import { DB } from '../database';
import logger from '../logger';
class Donations {
private notifyDonationStatusCallback: ((invoiceId: string) => void) | undefined;
private options = {
baseURL: config.SPONSORS.BTCPAY_URL,
headers: {
'Content-Type': 'application/json',
'Authorization': config.SPONSORS.BTCPAY_AUTH,
},
};
sponsorsCache: any[] = [];
constructor() {
if (!config.SPONSORS.ENABLED) {
return;
}
this.$updateCache();
}
async $updateCache() {
try {
this.sponsorsCache = await this.$getDonationsFromDatabase('handle, image');
} catch (e) {
logger.warn('Setting sponsorsCache failed ' + e.message || e);
}
}
setNotfyDonationStatusCallback(fn: any): void {
this.notifyDonationStatusCallback = fn;
}
async $createRequest(amount: number, orderId: string): Promise<any> {
logger.notice('New invoice request. Handle: ' + orderId + ' Amount: ' + amount + ' BTC');
const postData = {
'price': amount,
'orderId': orderId,
'currency': 'BTC',
'itemDesc': 'Sponsor mempool.space',
'notificationUrl': config.SPONSORS.BTCPAY_WEBHOOK_URL,
'redirectURL': 'https://mempool.space/about',
};
const response = await axios.post('/invoices', postData, this.options);
return {
id: response.data.data.id,
amount: parseFloat(response.data.data.btcPrice),
addresses: response.data.data.addresses,
};
}
async $handleWebhookRequest(data: any): Promise<void> {
if (!data || !data.id) {
return;
}
const response = await this.$getStatus(data.id);
logger.notice(`Received BTCPayServer webhook. Invoice ID: ${data.id} Status: ${response.status} BTC Paid: ${response.btcPaid}`);
if (response.status !== 'complete' && response.status !== 'confirmed' && response.status !== 'paid') {
return;
}
if (this.notifyDonationStatusCallback) {
this.notifyDonationStatusCallback(data.id);
}
if (parseFloat(response.btcPaid) < 0.01) {
return;
}
if (response.orderId !== '') {
try {
const userData = await this.$getTwitterUserData(response.orderId);
const imageUrl = userData.profile_image_url.replace('normal', '200x200');
const imageBlob = await this.$downloadProfileImageBlob(imageUrl);
logger.debug('Creating database entry for donation with invoice id: ' + response.id);
await this.$addDonationToDatabase(response.btcPaid, userData.screen_name, userData.id, response.id, imageUrl, imageBlob);
this.$updateCache();
} catch (e) {
logger.err(`Error fetching twitter data for handle ${response.orderId}: ${e.message}`);
}
}
}
getSponsorImage(id: string): any | undefined {
const sponsor = this.sponsorsCache.find((s) => s.handle === id);
if (sponsor) {
return sponsor.image;
}
}
async $getDonationsFromDatabase(fields: string): Promise<any[]> {
try {
const connection = await DB.pool.getConnection();
const query = `SELECT ${fields} FROM donations ORDER BY id DESC`;
const [rows] = await connection.query<any>(query);
connection.release();
return rows;
} catch (e) {
logger.err('$getDonationsFromDatabase() error: ' + e.message || e);
return [];
}
}
private async $getOldDonations(): Promise<any[]> {
try {
const connection = await DB.pool.getConnection();
const query = `SELECT * FROM donations WHERE twitter_id IS NULL AND handle != ''`;
const [rows] = await connection.query<any>(query);
connection.release();
return rows;
} catch (e) {
logger.err('$getLegacyDonations() error' + e.message || e);
return [];
}
}
private async $getStatus(id: string): Promise<any> {
logger.debug('Fetching status for invoice: ' + id);
const response = await axios.get('/invoices/' + id, this.options);
logger.debug('Invoice status received: ' + JSON.stringify(response.data));
return response.data.data;
}
private async $addDonationToDatabase(btcPaid: number, handle: string, twitter_id: number | null,
orderId: string, imageUrl: string, image: string): Promise<void> {
try {
const connection = await DB.pool.getConnection();
const query = `INSERT IGNORE INTO donations(added, amount, handle, twitter_id, order_id, imageUrl, image) VALUES (NOW(), ?, ?, ?, ?, ?, FROM_BASE64(?))`;
const params: (string | number | null)[] = [
btcPaid,
handle,
twitter_id,
orderId,
imageUrl,
image,
];
const [result]: any = await connection.query(query, params);
connection.release();
} catch (e) {
logger.err('$addDonationToDatabase() error' + e.message || e);
}
}
private async $updateDonation(id: number, handle: string, twitterId: number, imageUrl: string, image: string): Promise<void> {
try {
const connection = await DB.pool.getConnection();
const query = `UPDATE donations SET handle = ?, twitter_id = ?, imageUrl = ?, image = FROM_BASE64(?) WHERE id = ?`;
const params: (string | number)[] = [
handle,
twitterId,
imageUrl,
image,
id,
];
const [result]: any = await connection.query(query, params);
connection.release();
} catch (e) {
logger.err('$updateDonation() error' + e.message || e);
}
}
private async $getTwitterUserData(handle: string): Promise<any> {
logger.debug('Fetching Twitter API data...');
const res = await axios.get(`https://api.twitter.com/1.1/users/show.json?screen_name=${handle}`, {
headers: {
Authorization: 'Bearer ' + config.SPONSORS.TWITTER_BEARER_AUTH
}
});
logger.debug('Twitter user data fetched:' + JSON.stringify(res.data));
return res.data;
}
private async $downloadProfileImageBlob(url: string): Promise<string> {
logger.debug('Fetching image blob...');
const res = await axios.get(url, { responseType: 'arraybuffer' });
logger.debug('Image downloaded.');
return Buffer.from(res.data, 'utf8').toString('base64');
}
private async refreshSponsors(): Promise<void> {
const oldDonations = await this.$getOldDonations();
oldDonations.forEach(async (donation: any) => {
logger.debug('Migrating donation for handle: ' + donation.handle);
try {
const twitterData = await this.$getTwitterUserData(donation.handle);
const imageUrl = twitterData.profile_image_url.replace('normal', '200x200');
const imageBlob = await this.$downloadProfileImageBlob(imageUrl);
await this.$updateDonation(donation.id, twitterData.screen_name, twitterData.id, imageUrl, imageBlob);
} catch (e) {
logger.err('Failed to migrate donation for handle: ' + donation.handle + '. ' + (e.message || e));
}
});
}
}
export default new Donations();

View File

@@ -1,5 +1,6 @@
import config from '../config';
import { MempoolBlock } from '../mempool.interfaces';
import mempool from './mempool';
import projectedBlocks from './mempool-blocks';
class FeeApi {
@@ -9,32 +10,36 @@ class FeeApi {
public getRecommendedFee() {
const pBlocks = projectedBlocks.getMempoolBlocks();
const mPool = mempool.getMempoolInfo();
const minimumFee = Math.ceil(mPool.mempoolminfee * 100000);
if (!pBlocks.length) {
return {
'fastestFee': this.defaultFee,
'halfHourFee': this.defaultFee,
'hourFee': this.defaultFee,
'minimumFee': minimumFee,
};
}
const firstMedianFee = this.optimizeMedianFee(pBlocks[0]);
const secondMedianFee = pBlocks[1] ? this.optimizeMedianFee(pBlocks[1], firstMedianFee) : this.defaultFee;
const thirdMedianFee = pBlocks[2] ? this.optimizeMedianFee(pBlocks[2], secondMedianFee) : this.defaultFee;
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,
'halfHourFee': secondMedianFee,
'hourFee': thirdMedianFee,
'minimumFee': minimumFee,
};
}
private optimizeMedianFee(pBlock: MempoolBlock, previousFee?: number): number {
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) {
if (pBlock.blockVSize <= 950000 && !nextBlock) {
const multiplier = (pBlock.blockVSize - 500000) / 500000;
return Math.max(Math.round(useFee * multiplier), this.defaultFee);
}

View File

@@ -26,7 +26,7 @@ class FiatConversion {
private async updateCurrency(): Promise<void> {
try {
const response = await axios.get('https://price.bisq.wiz.biz/getAllMarketPrices');
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,

View File

@@ -1,5 +1,7 @@
import logger from '../logger';
import { MempoolBlock, TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
import { Common } from './common';
import config from '../config';
class MempoolBlocks {
private static DEFAULT_PROJECTED_BLOCKS_AMOUNT = 8;
@@ -32,9 +34,40 @@ class MempoolBlocks {
memPoolArray.push(latestMempool[i]);
}
}
const start = new Date().getTime();
// Clear bestDescendants & ancestors
memPoolArray.forEach((tx) => {
tx.bestDescendant = null;
tx.ancestors = [];
tx.cpfpChecked = false;
if (!tx.effectiveFeePerVsize) {
tx.effectiveFeePerVsize = tx.feePerVsize;
}
});
// First sort
memPoolArray.sort((a, b) => b.feePerVsize - a.feePerVsize);
const transactionsSorted = memPoolArray.filter((tx) => tx.feePerVsize);
this.mempoolBlocks = this.calculateMempoolBlocks(transactionsSorted);
// Loop through and traverse all ancestors and sum up all the sizes + fees
// Pass down size + fee to all unconfirmed children
let sizes = 0;
memPoolArray.forEach((tx, i) => {
sizes += tx.weight;
if (sizes > 4000000 * 8) {
return;
}
Common.setRelativesAndGetCpfpInfo(tx, memPool);
});
// Final sort, by effective fee
memPoolArray.sort((a, b) => b.effectiveFeePerVsize - a.effectiveFeePerVsize);
const end = new Date().getTime();
const time = end - start;
logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds');
this.mempoolBlocks = this.calculateMempoolBlocks(memPoolArray);
}
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] {
@@ -76,7 +109,7 @@ class MempoolBlocks {
blockVSize: blockVSize,
nTx: transactions.length,
totalFees: transactions.reduce((acc, cur) => acc + cur.fee, 0),
medianFee: Common.median(transactions.map((tx) => tx.feePerVsize)),
medianFee: Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
feeRange: Common.getFeesInRange(transactions, rangeLength),
transactionIds: transactions.map((tx) => tx.txid),
};

View File

@@ -10,10 +10,11 @@ import loadingIndicators from './loading-indicators';
class Mempool {
private static WEBSOCKET_REFRESH_RATE_MS = 10000;
private static LAZY_DELETE_AFTER_SECONDS = 30;
private inSync: boolean = false;
private mempoolCache: { [txId: string]: TransactionExtended } = {};
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0,
maxmempool: 0, mempoolminfee: 0, minrelaytxfee: 0 };
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
deletedTransactions: TransactionExtended[]) => void) | undefined;
@@ -27,12 +28,18 @@ class Mempool {
constructor() {
setInterval(this.updateTxPerSecond.bind(this), 1000);
setInterval(this.deleteExpiredTransactions.bind(this), 20000);
}
public isInSync() {
public isInSync(): boolean {
return this.inSync;
}
public setOutOfSync(): void {
this.inSync = false;
loadingIndicators.setProgress('mempool', 99);
}
public getLatestTransactions() {
return this.latestTransactions;
}
@@ -57,7 +64,7 @@ class Mempool {
this.mempoolInfo = await bitcoinBaseApi.$getMempoolInfo();
}
public getMempoolInfo(): IBitcoinApi.MempoolInfo | undefined {
public getMempoolInfo(): IBitcoinApi.MempoolInfo {
return this.mempoolInfo;
}
@@ -98,8 +105,8 @@ class Mempool {
for (const txid of transactions) {
if (!this.mempoolCache[txid]) {
const transaction = await transactionUtils.$getTransactionExtended(txid, true);
if (transaction) {
try {
const transaction = await transactionUtils.$getTransactionExtended(txid);
this.mempoolCache[txid] = transaction;
txCount++;
if (this.inSync) {
@@ -116,8 +123,8 @@ class Mempool {
logger.debug('Fetched transaction ' + txCount);
}
newTransactions.push(transaction);
} else {
logger.debug('Error finding transaction in mempool.');
} catch (e) {
logger.debug('Error finding transaction in mempool: ' + e.message || e);
}
}
@@ -128,7 +135,6 @@ class Mempool {
// Prevent mempool from clear on bitcoind restart by delaying the deletion
if (this.mempoolProtection === 0
&& config.MEMPOOL.BACKEND === 'esplora'
&& currentMempoolSize > 20000
&& transactions.length / currentMempoolSize <= 0.80
) {
@@ -138,10 +144,9 @@ class Mempool {
setTimeout(() => {
this.mempoolProtection = 2;
logger.warn('Mempool clear protection resumed.');
}, 1000 * 60 * 2);
}, 1000 * 60 * config.MEMPOOL.CLEAR_PROTECTION_MINUTES);
}
let newMempool = {};
const deletedTransactions: TransactionExtended[] = [];
if (this.mempoolProtection !== 1) {
@@ -150,35 +155,31 @@ class Mempool {
const transactionsObject = {};
transactions.forEach((txId) => transactionsObject[txId] = true);
// Replace mempool to separate deleted transactions
// Flag transactions for lazy deletion
for (const tx in this.mempoolCache) {
if (transactionsObject[tx]) {
newMempool[tx] = this.mempoolCache[tx];
} else {
if (!transactionsObject[tx] && !this.mempoolCache[tx].deleteAfter) {
deletedTransactions.push(this.mempoolCache[tx]);
this.mempoolCache[tx].deleteAfter = new Date().getTime() + Mempool.LAZY_DELETE_AFTER_SECONDS * 1000;
}
}
} 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) {
if (!this.inSync && transactions.length === Object.keys(this.mempoolCache).length) {
this.inSync = true;
logger.info('The mempool is now in sync!');
logger.notice('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(`New mempool size: ${Object.keys(this.mempoolCache).length} Change: ${diff}`);
logger.debug('Mempool updated in ' + time / 1000 + ' seconds');
}
@@ -194,6 +195,16 @@ class Mempool {
);
}
}
private deleteExpiredTransactions() {
const now = new Date().getTime();
for (const tx in this.mempoolCache) {
const lazyDeleteAt = this.mempoolCache[tx].deleteAfter;
if (lazyDeleteAt && lazyDeleteAt < now) {
delete this.mempoolCache[tx];
}
}
}
}
export default new Mempool();

View File

@@ -7,6 +7,10 @@ import { Statistic, TransactionExtended, OptimizedStatistic } from '../mempool.i
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;
@@ -28,6 +32,23 @@ class Statistics {
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> {
@@ -47,13 +68,13 @@ class Statistics {
}
}
// Remove 0 and undefined
memPoolArray = memPoolArray.filter((tx) => tx.feePerVsize);
memPoolArray = memPoolArray.filter((tx) => tx.effectiveFeePerVsize);
if (!memPoolArray.length) {
return;
}
memPoolArray.sort((a, b) => a.feePerVsize - b.feePerVsize);
memPoolArray.sort((a, b) => a.effectiveFeePerVsize - b.effectiveFeePerVsize);
const totalWeight = memPoolArray.map((tx) => tx.vsize).reduce((acc, curr) => acc + curr) * 4;
const totalFee = memPoolArray.map((tx) => tx.fee).reduce((acc, curr) => acc + curr);
@@ -64,7 +85,7 @@ class Statistics {
memPoolArray.forEach((transaction) => {
for (let i = 0; i < logFees.length; i++) {
if ((logFees[i] === 2000 && transaction.feePerVsize >= 2000) || transaction.feePerVsize <= logFees[i]) {
if ((logFees[i] === 2000 && transaction.effectiveFeePerVsize >= 2000) || transaction.effectiveFeePerVsize <= logFees[i]) {
if (weightVsizeFees[logFees[i]]) {
weightVsizeFees[logFees[i]] += transaction.vsize;
} else {
@@ -300,7 +321,7 @@ 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) {
@@ -313,7 +334,7 @@ class Statistics {
try {
const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(180);
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) {
@@ -326,7 +347,7 @@ class Statistics {
try {
const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(1260);
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) {
@@ -339,7 +360,7 @@ class Statistics {
try {
const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(5040);
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) {
@@ -352,7 +373,7 @@ class Statistics {
try {
const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(15120);
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) {
@@ -365,7 +386,7 @@ class Statistics {
try {
const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(30240);
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) {
@@ -378,7 +399,7 @@ class Statistics {
try {
const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(60480);
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) {

View File

@@ -20,26 +20,22 @@ class TransactionUtils {
};
}
public async $getTransactionExtended(txId: string, forceBitcoind = false, addPrevouts = false): Promise<TransactionExtended | null> {
try {
let transaction: IEsploraApi.Transaction;
if (forceBitcoind) {
transaction = await bitcoinApi.$getRawTransactionBitcoind(txId, false, addPrevouts);
} else {
transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts);
}
return this.extendTransaction(transaction);
} catch (e) {
logger.debug('getTransactionExtended error: ' + (e.message || e));
logger.debug(JSON.stringify(e));
return null;
}
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 {
// @ts-ignore
if (transaction.vsize) {
// @ts-ignore
return transaction;
}
const feePerVbytes = Math.max(1, (transaction.fee || 0) / (transaction.weight / 4));
const transactionExtended: TransactionExtended = Object.assign({
vsize: Math.round(transaction.weight / 4),
feePerVsize: Math.max(1, (transaction.fee || 0) / (transaction.weight / 4)),
feePerVsize: feePerVbytes,
effectiveFeePerVsize: feePerVbytes,
}, transaction);
if (!transaction.status.confirmed) {
transactionExtended.firstSeen = Math.round((new Date().getTime() / 1000));

View File

@@ -159,8 +159,7 @@ class WebsocketHandler {
'conversions': fiatConversion.getConversionRates(),
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
'transactions': memPool.getLatestTransactions(),
'git-commit': backendInfo.gitCommitHash,
'hostname': backendInfo.hostname,
'backendInfo': backendInfo.getBackendInfo(),
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
...this.extraInitProperties
};
@@ -194,10 +193,15 @@ class WebsocketHandler {
mempoolBlocks.updateMempoolBlocks(newMempool);
const mBlocks = mempoolBlocks.getMempoolBlocks();
const mempool = memPool.getMempool();
const mempoolInfo = memPool.getMempoolInfo();
const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
for (const rbfTransaction in rbfTransactions) {
delete mempool[rbfTransaction];
}
this.wss.clients.forEach(async (client: WebSocket) => {
if (client.readyState !== WebSocket.OPEN) {
return;
@@ -219,9 +223,11 @@ class WebsocketHandler {
const tx = newTransactions.find((t) => t.txid === client['track-mempool-tx']);
if (tx) {
if (config.MEMPOOL.BACKEND !== 'esplora') {
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, false, true);
if (fullTx) {
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;
@@ -237,9 +243,11 @@ class WebsocketHandler {
const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_address === client['track-address']);
if (someVin) {
if (config.MEMPOOL.BACKEND !== 'esplora') {
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, false, true);
if (fullTx) {
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);
@@ -249,9 +257,11 @@ class WebsocketHandler {
const someVout = tx.vout.some((vout) => vout.scriptpubkey_address === client['track-address']);
if (someVout) {
if (config.MEMPOOL.BACKEND !== 'esplora') {
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, false, true);
if (fullTx) {
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);
@@ -298,9 +308,11 @@ class WebsocketHandler {
if (client['track-tx'] === rbfTransaction) {
const rbfTx = rbfTransactions[rbfTransaction];
if (config.MEMPOOL.BACKEND !== 'esplora') {
const fullTx = await transactionUtils.$getTransactionExtended(rbfTransaction, false, true);
if (fullTx) {
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;
@@ -321,28 +333,23 @@ class WebsocketHandler {
throw new Error('WebSocket.Server is not set');
}
// Check how many transactions in the new block matches the latest projected mempool block
// If it's more than 0, recalculate the mempool blocks and send to client in the same update
let mBlocks: undefined | MempoolBlock[];
let matchRate = 0;
const _memPool = memPool.getMempool();
const _mempoolBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
if (_mempoolBlocks[0]) {
const matches: string[] = [];
for (const txId of txIds) {
if (_mempoolBlocks[0].transactionIds.indexOf(txId) > -1) {
matches.push(txId);
}
delete _memPool[txId];
}
matchRate = Math.round((matches.length / (txIds.length - 1)) * 100);
if (matchRate > 0) {
const currentMemPool = memPool.getMempool();
for (const txId of matches) {
delete currentMemPool[txId];
}
mempoolBlocks.updateMempoolBlocks(currentMemPool);
mBlocks = mempoolBlocks.getMempoolBlocks();
}
mempoolBlocks.updateMempoolBlocks(_memPool);
mBlocks = mempoolBlocks.getMempoolBlocks();
}
block.matchRate = matchRate;

View File

@@ -2,12 +2,15 @@ const configFile = require('../mempool-config.json');
interface IConfig {
MEMPOOL: {
NETWORK: 'mainnet' | 'testnet' | 'liquid';
NETWORK: 'mainnet' | 'testnet' | 'signet' | '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;
RECOMMENDED_FEE_PERCENTILE: number;
};
ESPLORA: {
REST_API_URL: string;
@@ -16,7 +19,6 @@ interface IConfig {
HOST: string;
PORT: number;
TLS_ENABLED: boolean;
TX_LOOKUPS: boolean;
};
CORE_RPC: {
HOST: string;
@@ -24,6 +26,13 @@ interface IConfig {
USERNAME: string;
PASSWORD: string;
};
CORE_RPC_MINFEE: {
ENABLED: boolean;
HOST: string;
PORT: number;
USERNAME: string;
PASSWORD: string;
};
DATABASE: {
ENABLED: boolean;
HOST: string,
@@ -32,6 +41,13 @@ interface IConfig {
USERNAME: string;
PASSWORD: string;
};
SYSLOG: {
ENABLED: boolean;
HOST: string;
PORT: number;
MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' |'warn' | 'notice' | 'info' | 'debug';
FACILITY: string;
};
STATISTICS: {
ENABLED: boolean;
TX_PER_SECOND_SAMPLE_PERIOD: number;
@@ -44,13 +60,6 @@ interface IConfig {
ENABLED: boolean;
DATA_PATH: string;
};
SPONSORS: {
ENABLED: boolean;
BTCPAY_URL: string;
BTCPAY_AUTH: string;
BTCPAY_WEBHOOK_URL: string;
TWITTER_BEARER_AUTH: string;
};
}
const defaults: IConfig = {
@@ -60,7 +69,10 @@ const defaults: IConfig = {
'HTTP_PORT': 8999,
'SPAWN_CLUSTER_PROCS': 0,
'API_URL_PREFIX': '/api/v1/',
'POLL_RATE_MS': 2000
'POLL_RATE_MS': 2000,
'CACHE_DIR': './cache',
'CLEAR_PROTECTION_MINUTES': 20,
'RECOMMENDED_FEE_PERCENTILE': 50,
},
'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000',
@@ -69,7 +81,6 @@ const defaults: IConfig = {
'HOST': '127.0.0.1',
'PORT': 3306,
'TLS_ENABLED': true,
'TX_LOOKUPS': false
},
'CORE_RPC': {
'HOST': '127.0.0.1',
@@ -77,14 +88,28 @@ const defaults: IConfig = {
'USERNAME': 'mempool',
'PASSWORD': 'mempool'
},
'CORE_RPC_MINFEE': {
'ENABLED': false,
'HOST': '127.0.0.1',
'PORT': 8332,
'USERNAME': 'mempool',
'PASSWORD': 'mempool'
},
'DATABASE': {
'ENABLED': true,
'HOST': 'localhost',
'HOST': '127.0.0.1',
'PORT': 3306,
'DATABASE': 'mempool',
'USERNAME': 'mempool',
'PASSWORD': 'mempool'
},
'SYSLOG': {
'ENABLED': true,
'HOST': '127.0.0.1',
'PORT': 514,
'MIN_PRIORITY': 'info',
'FACILITY': 'local7'
},
'STATISTICS': {
'ENABLED': true,
'TX_PER_SECOND_SAMPLE_PERIOD': 150
@@ -97,13 +122,6 @@ const defaults: IConfig = {
'ENABLED': false,
'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db'
},
'SPONSORS': {
'ENABLED': false,
'BTCPAY_URL': '',
'BTCPAY_AUTH': '',
'BTCPAY_WEBHOOK_URL': '',
'TWITTER_BEARER_AUTH': ''
}
};
class Config implements IConfig {
@@ -111,11 +129,12 @@ class Config implements IConfig {
ESPLORA: IConfig['ESPLORA'];
ELECTRUM: IConfig['ELECTRUM'];
CORE_RPC: IConfig['CORE_RPC'];
CORE_RPC_MINFEE: IConfig['CORE_RPC_MINFEE'];
DATABASE: IConfig['DATABASE'];
SYSLOG: IConfig['SYSLOG'];
STATISTICS: IConfig['STATISTICS'];
BISQ_BLOCKS: IConfig['BISQ_BLOCKS'];
BISQ_MARKETS: IConfig['BISQ_MARKETS'];
SPONSORS: IConfig['SPONSORS'];
constructor() {
const configs = this.merge(configFile, defaults);
@@ -123,11 +142,12 @@ class Config implements IConfig {
this.ESPLORA = configs.ESPLORA;
this.ELECTRUM = configs.ELECTRUM;
this.CORE_RPC = configs.CORE_RPC;
this.CORE_RPC_MINFEE = configs.CORE_RPC_MINFEE;
this.DATABASE = configs.DATABASE;
this.SYSLOG = configs.SYSLOG;
this.STATISTICS = configs.STATISTICS;
this.BISQ_BLOCKS = configs.BISQ_BLOCKS;
this.BISQ_MARKETS = configs.BISQ_MARKETS;
this.SPONSORS = configs.SPONSORS;
}
merge = (...objects: object[]): IConfig => {

View File

@@ -1,7 +1,6 @@
import { Express, Request, Response, NextFunction } from 'express';
import * as express from 'express';
import * as http from 'http';
import * as https from 'https';
import * as WebSocket from 'ws';
import * as cluster from 'cluster';
import axios from 'axios';
@@ -17,14 +16,14 @@ import websocketHandler from './api/websocket-handler';
import fiatConversion from './api/fiat-conversion';
import bisq from './api/bisq/bisq';
import bisqMarkets from './api/bisq/markets';
import donations from './api/donations';
import logger from './logger';
import backendInfo from './api/backend-info';
import loadingIndicators from './api/loading-indicators';
import mempool from './api/mempool';
class Server {
private wss: WebSocket.Server | undefined;
private server: https.Server | http.Server | undefined;
private server: http.Server | undefined;
private app: Express;
private currentBackendRetryInterval = 5;
@@ -60,7 +59,9 @@ class Server {
}
}
startServer(worker = false) {
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', '*');
@@ -72,20 +73,22 @@ class Server {
this.server = http.createServer(this.app);
this.wss = new WebSocket.Server({ server: this.server });
this.setUpWebsocketHandling();
diskCache.loadMempoolCache();
if (config.DATABASE.ENABLED) {
checkDbConnection();
await checkDbConnection();
}
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
statistics.startStatistics();
}
this.setUpHttpApiRoutes();
this.setUpWebsocketHandling();
this.runMainUpdateLoop();
fiatConversion.startService();
diskCache.loadMempoolCache();
this.setUpHttpApiRoutes();
this.runMainUpdateLoop();
if (config.BISQ_BLOCKS.ENABLED) {
bisq.startBisqService();
@@ -101,14 +104,23 @@ class Server {
if (worker) {
logger.info(`Mempool Server worker #${process.pid} started`);
} else {
logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT} (${backendInfo.getShortCommitHash()})`);
logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`);
}
});
}
async runMainUpdateLoop() {
try {
await memPool.$updateMemPoolInfo();
try {
await memPool.$updateMemPoolInfo();
} catch (e) {
const msg = `updateMempoolInfo: ${(e.message || e)}`;
if (config.CORE_RPC_MINFEE.ENABLED) {
logger.warn(msg);
} else {
logger.debug(msg);
}
}
await blocks.$updateBlocks();
await memPool.$updateMempool();
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
@@ -117,6 +129,7 @@ class Server {
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);
}
@@ -135,7 +148,6 @@ class Server {
statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler));
blocks.setNewBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
memPool.setMempoolChangedCallback(websocketHandler.handleMempoolChange.bind(websocketHandler));
donations.setNotfyDonationStatusCallback(websocketHandler.handleNewDonation.bind(websocketHandler));
fiatConversion.setProgressChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
}
@@ -143,10 +155,47 @@ class Server {
setUpHttpApiRoutes() {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', routes.getTransactionTimes)
.get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', routes.getCpfpInfo)
.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();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors', async (req, res) => {
try {
const response = await axios.get('https://mempool.space/api/v1/contributors', { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors/images/:id', async (req, res) => {
try {
const response = await axios.get('https://mempool.space/api/v1/contributors/images/' + req.params.id, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
;
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
@@ -186,41 +235,21 @@ class Server {
;
}
if (config.SPONSORS.ENABLED) {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', routes.getDonations.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', routes.getSponsorImage.bind(routes))
.post(config.MEMPOOL.API_URL_PREFIX + 'donations', routes.createDonationRequest.bind(routes))
.post(config.MEMPOOL.API_URL_PREFIX + 'donations-webhook', routes.donationWebhook.bind(routes))
;
} else {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
try {
const response = await axios.get('https://mempool.space/api/v1/donations', { responseType: 'stream' });
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' });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
});
}
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/:height', routes.getBlocks)
.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)

View File

@@ -50,17 +50,11 @@ class Logger {
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) {
constructor() {
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);
@@ -97,10 +91,12 @@ class Logger {
}
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 (config.SYSLOG.ENABLED && Logger.priorities[priority] <= Logger.priorities[config.SYSLOG.MIN_PRIORITY]) {
syslogmsg = `<${(Logger.facilities[config.SYSLOG.FACILITY] * 8 + prionum)}> ${this.name}[${process.pid}]: ${priority.toUpperCase()}${network} ${msg}`;
this.syslog(syslogmsg);
}
if (priority === 'warning') {
priority = 'warn';
}
@@ -116,7 +112,7 @@ class Logger {
private syslog(msg) {
let msgbuf;
msgbuf = Buffer.from(msg);
this.client.send(msgbuf, 0, msgbuf.length, this.logport, this.loghost, function(err, bytes) {
this.client.send(msgbuf, 0, msgbuf.length, config.SYSLOG.PORT, config.SYSLOG.HOST, function(err, bytes) {
if (err) {
console.log(err);
}
@@ -146,4 +142,4 @@ class Logger {
}
}
export default new Logger(Logger.facilities.local7);
export default new Logger();

View File

@@ -26,12 +26,34 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
vsize: number;
feePerVsize: number;
firstSeen?: number;
effectiveFeePerVsize: number;
ancestors?: Ancestor[];
bestDescendant?: BestDescendant | null;
cpfpChecked?: boolean;
deleteAfter?: number;
}
interface Ancestor {
txid: string;
weight: number;
fee: number;
}
interface BestDescendant {
txid: string;
weight: number;
fee: number;
}
export interface CpfpInfo {
ancestors: Ancestor[];
bestDescendant: BestDescendant | null;
}
export interface TransactionStripped {
txid: string;
fee: number;
weight: number;
vsize: number;
value: number;
}
export interface BlockExtended extends IEsploraApi.Block {
@@ -138,3 +160,9 @@ interface RequiredParams {
export interface ILoadingIndicators { [name: string]: number; }
export interface IConversionRates { [currency: string]: number; }
export interface IBackendInfo {
hostname: string;
gitCommit: string;
version: string;
}

View File

@@ -8,37 +8,18 @@ import mempool from './api/mempool';
import bisq from './api/bisq/bisq';
import websocketHandler from './api/websocket-handler';
import bisqMarket from './api/bisq/markets-api';
import { OptimizedStatistic, RequiredSpec, TransactionExtended } from './mempool.interfaces';
import { RequiredSpec, TransactionExtended } from './mempool.interfaces';
import { MarketsApiError } from './api/bisq/interfaces';
import { IEsploraApi } from './api/bitcoin/esplora-api.interface';
import donations from './api/donations';
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: { [date: string]: OptimizedStatistic[] } = {
'24h': [], '1w': [], '1m': [], '3m': [], '6m': [], '1y': [],
};
constructor() {
if (config.DATABASE.ENABLED && config.STATISTICS.ENABLED) {
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();
logger.debug('Statistics cache created');
}
constructor() {}
public async get2HStatistics(req: Request, res: Response) {
const result = await statistics.$list2H();
@@ -46,27 +27,27 @@ 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) {
@@ -113,83 +94,35 @@ class Routes {
res.json(times);
}
public getCpfpInfo(req: Request, res: Response) {
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
res.status(501).send(`Invalid transaction ID.`);
return;
}
const tx = mempool.getMempool()[req.params.txId];
if (!tx) {
res.status(404).send(`Transaction doesn't exist in the mempool.`);
return;
}
if (tx.cpfpChecked) {
res.json({
ancestors: tx.ancestors,
bestDescendant: tx.bestDescendant || null,
});
return;
}
const cpfpInfo = Common.setRelativesAndGetCpfpInfo(tx, mempool.getMempool());
res.json(cpfpInfo);
}
public getBackendInfo(req: Request, res: Response) {
res.json(backendInfo.getBackendInfo());
}
public async createDonationRequest(req: Request, res: Response) {
const constraints: RequiredSpec = {
'amount': {
required: true,
types: ['@float']
},
'orderId': {
required: true,
types: ['@string']
}
};
const p = this.parseRequestParameters(req.body, constraints);
if (p.error) {
res.status(400).send(p.error);
return;
}
if (p.orderId !== '' && !/^(@|)[a-zA-Z0-9_]{1,15}$/.test(p.orderId)) {
res.status(400).send('Invalid Twitter handle');
return;
}
if (p.amount < 0.001) {
res.status(400).send('Amount needs to be at least 0.001');
return;
}
if (p.amount > 1000) {
res.status(400).send('Amount too large');
return;
}
try {
const result = await donations.$createRequest(p.amount, p.orderId);
res.json(result);
} catch (e) {
res.status(500).send(e.message);
}
}
public async getDonations(req: Request, res: Response) {
try {
const result = await donations.$getDonationsFromDatabase('handle, imageUrl');
res.json(result);
} catch (e) {
res.status(500).send(e.message);
}
}
public async getSponsorImage(req: Request, res: Response) {
try {
const result = await donations.getSponsorImage(req.params.id);
if (result) {
res.set('Content-Type', 'image/jpeg');
res.send(result);
} else {
res.status(404).end();
}
} catch (e) {
res.status(500).send(e.message);
}
}
public async donationWebhook(req: Request, res: Response) {
try {
donations.$handleWebhookRequest(req.body);
res.end();
} catch (e) {
res.status(500).send(e);
}
}
public getBisqStats(req: Request, res: Response) {
const result = bisq.getStats();
res.json(result);
@@ -531,15 +464,27 @@ class Routes {
public async getTransaction(req: Request, res: Response) {
try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, false, true);
if (transaction) {
res.json(transaction);
} else {
res.status(500).send('Error fetching transaction.');
}
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
res.json(transaction);
} catch (e) {
res.status(500).send(e.message || 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);
}
}
@@ -595,14 +540,16 @@ class Routes {
const txIds = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
const transactions: TransactionExtended[] = [];
const startingIndex = Math.max(0, parseInt(req.params.index, 10));
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++) {
const transaction = await transactionUtils.$getTransactionExtended(txIds[i], false, true);
if (transaction) {
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);
@@ -668,6 +615,45 @@ class Routes {
}
}
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');
}

101
docker/README.md Normal file
View File

@@ -0,0 +1,101 @@
# 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"
ELECTRUM_HOST: "127.0.0.1"
ELECTRUM_PORT: "50002"
ELECTRUM_TLS: "false"
MYSQL_HOST: "db"
MYSQL_PORT: "3306"
MYSQL_DATABASE: "mempool"
MYSQL_USER: "mempool"
MYSQL_PASS: "mempool"
BACKEND_MAINNET_HTTP_PORT: "8999"
CACHE_DIR: "/backend/cache"
MEMPOOL_CLEAR_PROTECTION_MINUTES: "20"
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 ELECTRUM 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__",
"CLEAR_PROTECTION_MINUTES": __MEMPOOL_BACKEND_CLEAR_PROTECTION_MINUTES__
},
"CORE_RPC": {
"HOST": "__BITCOIN_MAINNET_RPC_HOST__",
"PORT": __BITCOIN_MAINNET_RPC_PORT__,
"USERNAME": "__BITCOIN_MAINNET_RPC_USER__",
"PASSWORD": "__BITCOIN_MAINNET_RPC_PASS__"
},
"ELECTRUM": {
"HOST": "__ELECTRUM_MAINNET_HTTP_HOST__",
"PORT": __ELECTRUM_MAINNET_HTTP_PORT__,
"TLS_ENABLED": __ELECTRUM_MAINNET_TLS_ENABLED__
},
"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
}
}

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

@@ -0,0 +1,41 @@
#!/bin/sh
#MEMPOOL
__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__=${BACKEND_MAINNET_HTTP_PORT:=8999}
__MEMPOOL_BACKEND_MAINNET_CACHE_DIR__=${CACHE_DIR:=./cache}
__MEMPOOL_BACKEND_CLEAR_PROTECTION_MINUTES__=${MEMPOOL_CLEAR_PROTECTION_MINUTES:=20}
# 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
__ELECTRUM_MAINNET_HTTP_HOST__=${ELECTRUM_HOST:=127.0.0.1}
__ELECTRUM_MAINNET_HTTP_PORT__=${ELECTRUM_PORT:=50002} # 50001?
__ELECTRUM_MAINNET_TLS_ENABLED__=${ELECTRUM_TLS:=false}
# 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/__ELECTRUM_MAINNET_HTTP_HOST__/${__ELECTRUM_MAINNET_HTTP_HOST__}/g" mempool-config.json
sed -i "s/__ELECTRUM_MAINNET_HTTP_PORT__/${__ELECTRUM_MAINNET_HTTP_PORT__}/g" mempool-config.json
sed -i "s/__ELECTRUM_MAINNET_TLS_ENABLED__/${__ELECTRUM_MAINNET_TLS_ENABLED__}/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
sed -i "s/__MEMPOOL_BACKEND_CLEAR_PROTECTION_MINUTES__/${__MEMPOOL_BACKEND_CLEAR_PROTECTION_MINUTES__}/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

67
docker/docker-compose.yml Normal file
View File

@@ -0,0 +1,67 @@
version: "3.7"
services:
electrum:
build:
context: .
dockerfile: docker/electrum/Dockerfile
user: "1000:1000"
restart: on-failure
command: ""
ports:
- 50001:50001
- 50002:50002
- 4224:4224
- 8332:8332
environment:
ELECTRUM: "electrum"
# add electrs configs
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"
ELECTRUM_HOST: "127.0.0.1"
ELECTRUM_PORT: "50002"
ELECTRUM_TLS: "false"
MYSQL_HOST: "db"
MYSQL_PORT: "3306"
MYSQL_DATABASE: "mempool"
MYSQL_USER: "mempool"
MYSQL_PASS: "mempool"
BACKEND_MAINNET_HTTP_PORT: "8999"
CACHE_DIR: "/backend/cache"
MEMPOOL_CLEAR_PROTECTION_MINUTES: "20"
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"

View File

@@ -0,0 +1,32 @@
FROM ubuntu:18.04
MAINTAINER mempool.space developers
EXPOSE 50002
# runs as UID 1000 GID 1000 inside the container
ENV VERSION 4.0.9
RUN set -x \
&& apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends gpg gpg-agent dirmngr \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends wget xpra python3-pyqt5 python3-wheel python3-pip python3-setuptools libsecp256k1-0 libsecp256k1-dev python3-numpy python3-dev build-essential \
&& wget -O /tmp/Electrum-${VERSION}.tar.gz https://download.electrum.org/${VERSION}/Electrum-${VERSION}.tar.gz \
&& wget -O /tmp/Electrum-${VERSION}.tar.gz.asc https://download.electrum.org/${VERSION}/Electrum-${VERSION}.tar.gz.asc \
&& gpg --keyserver keys.gnupg.net --recv-keys 6694D8DE7BE8EE5631BED9502BD5824B7F9470E6 \
&& gpg --verify /tmp/Electrum-${VERSION}.tar.gz.asc /tmp/Electrum-${VERSION}.tar.gz \
&& pip3 install /tmp/Electrum-${VERSION}.tar.gz \
&& test -f /usr/local/bin/electrum \
&& rm -vrf /tmp/Electrum-${VERSION}.tar.gz /tmp/Electrum-${VERSION}.tar.gz.asc ${HOME}/.gnupg \
&& apt-get purge --autoremove -y python3-wheel python3-pip python3-setuptools python3-dev build-essential libsecp256k1-dev curl gpg gpg-agent dirmngr \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
&& useradd -d /home/mempool -m mempool \
&& mkdir /electrum \
&& ln -s /electrum /home/mempool/.electrum \
&& chown mempool:mempool /electrum
USER mempool
ENV HOME /home/mempool
WORKDIR /home/mempool
VOLUME /electrum
CMD ["/usr/bin/xpra", "start", ":100", "--start-child=/usr/local/bin/electrum", "--bind-tcp=0.0.0.0:50002","--daemon=yes", "--notifications=no", "--mdns=no", "--pulseaudio=no", "--html=off", "--speaker=disabled", "--microphone=disabled", "--webcam=no", "--printing=no", "--dbus-launch=", "--exit-with-children"]
ENTRYPOINT ["electrum"]

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

@@ -15,7 +15,10 @@ https://www.transifex.com/mempool/mempool/dashboard/
* Persian @techmix
* French @Bayernatoor
* Korean @kcalvinalvinn
* Italian @HodlBits
* Hebrew @Sh0ham
* Georgian @wyd_idk
* Hungarian @btcdragonlord
* Dutch @m__btc
* Japanese @wiz @japananon
* Norwegian @T82771355

View File

@@ -55,6 +55,14 @@
"translation": "src/locale/messages.ko.xlf",
"baseHref": "/ko/"
},
"it": {
"translation": "src/locale/messages.it.xlf",
"baseHref": "/it/"
},
"he": {
"translation": "src/locale/messages.he.xlf",
"baseHref": "/he/"
},
"nl": {
"translation": "src/locale/messages.nl.xlf",
"baseHref": "/nl/"
@@ -91,6 +99,10 @@
"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/"
@@ -131,7 +143,6 @@
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,

View File

@@ -5,6 +5,8 @@ const GENERATED_CONFIG_FILE_NAME = 'generated-config.js';
let settings = [];
let configContent = {};
let gitCommitHash = '';
let packetJsonVersion = '';
try {
const rawConfig = fs.readFileSync(CONFIG_FILE_NAME);
@@ -15,6 +17,13 @@ try {
}
}
try {
const packageJson = fs.readFileSync('package.json');
packetJsonVersion = JSON.parse(packageJson).version;
} catch (e) {
throw new Error(e);
}
for (setting in configContent) {
settings.push({
key: setting,
@@ -22,9 +31,17 @@ for (setting in configContent) {
});
}
try {
gitCommitHash = fs.readFileSync('../.git/refs/heads/master').toString().trim();
} catch (e) {
console.log('Could not load git commit info: ' + e.message || e);
}
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 };`, '')}
window.__env.GIT_COMMIT_HASH = '${gitCommitHash}';
window.__env.PACKAGE_JSON_VERSION = '${packetJsonVersion}';
}(global || this));`;
try {

View File

@@ -1,11 +1,11 @@
{
"TESTNET_ENABLED": false,
"SIGNET_ENABLED": false,
"LIQUID_ENABLED": false,
"BISQ_ENABLED": false,
"BISQ_SEPARATE_BACKEND": false,
"ITEMS_PER_PAGE": 10,
"KEEP_BLOCKS_AMOUNT": 8,
"SPONSORS_ENABLED": false,
"NGINX_PROTOCOL": "http",
"NGINX_HOSTNAME": "127.0.0.1",
"NGINX_PORT": "80"

15238
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
{
"name": "mempool-frontend",
"version": "2.0.1",
"version": "2.2.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "MIT",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",
"repository": {
"type": "git",
@@ -26,10 +26,11 @@
"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 --localize && npm run sync-assets",
"build": "npm run generate-config && ng build --prod --localize && npm run sync-assets && npm run build-mempool.js",
"sync-assets": "node sync-assets.js && rsync -av ./dist/mempool/browser/en-US/resources ./dist/mempool/browser/resources",
"sync-assets-dev": "node sync-assets.js dev",
"generate-config": "node generate-config.js",
"build-mempool.js": "tsc | browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index.js --standalone mempoolJS > ./dist/mempool/browser/en-US/mempool.js",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
@@ -39,58 +40,61 @@
"prerender": "ng run mempool:prerender"
},
"dependencies": {
"@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",
"@angular/animations": "~11.2.8",
"@angular/common": "~11.2.8",
"@angular/compiler": "~11.2.8",
"@angular/core": "~11.2.8",
"@angular/forms": "~11.2.8",
"@angular/localize": "^11.2.8",
"@angular/platform-browser": "~11.2.8",
"@angular/platform-browser-dynamic": "~11.2.8",
"@angular/platform-server": "~11.2.8",
"@angular/router": "~11.2.8",
"@fortawesome/angular-fontawesome": "^0.8.2",
"@fortawesome/fontawesome-common-types": "^0.2.35",
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@mempool/chartist": "^0.11.4",
"@mempool/mempool.js": "^2.2.0",
"@ng-bootstrap/ng-bootstrap": "^7.0.0",
"@nguniversal/express-engine": "10.1.0",
"@nguniversal/express-engine": "11.2.1",
"@types/qrcode": "^1.3.4",
"bootstrap": "4.5.0",
"browserify": "^17.0.0",
"clipboard": "^2.0.4",
"domino": "^2.1.6",
"express": "^4.15.2",
"express": "^4.17.1",
"ngx-bootrap-multiselect": "^2.0.0",
"ngx-infinite-scroll": "^9.0.0",
"ngx-infinite-scroll": "^10.0.1",
"qrcode": "^1.4.4",
"rxjs": "^6.6.3",
"rxjs": "^6.6.7",
"tinyify": "^3.0.0",
"tlite": "^0.1.9",
"tslib": "^2.0.0",
"zone.js": "~0.10.3"
"tslib": "^2.2.0",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@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",
"@angular-devkit/build-angular": "^0.1102.7",
"@angular/cli": "~11.2.7",
"@angular/compiler-cli": "~11.2.8",
"@angular/language-service": "~11.2.8",
"@nguniversal/builders": "^11.2.1",
"@types/express": "^4.17.0",
"@types/jasmine": "~3.3.8",
"@types/jasmine": "~3.6.0",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^12.11.1",
"codelyzer": "^6.0.0",
"codelyzer": "^6.0.1",
"http-proxy-middleware": "^1.0.5",
"jasmine-core": "~3.5.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.0.0",
"karma": "~6.1.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~3.3.0",
"karma-coverage": "~2.0.3",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~7.0.0",
"ts-node": "~7.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~4.0.5"
"typescript": "~4.1.5"
}
}

View File

@@ -37,6 +37,28 @@
"^/testnet/api": ""
}
},
"/signet/api/v1": {
"target": "http://localhost:8999/",
"secure": false,
"pathRewrite": {
"^/signet/api/v1": "/api/v1"
}
},
"/signet/api/v1/ws": {
"target": "http://localhost:8999/",
"secure": false,
"ws": true,
"pathRewrite": {
"^/signet/api": "/api/v1/ws"
}
},
"/signet/api/": {
"target": "http://localhost:50001/",
"secure": false,
"pathRewrite": {
"^/signet/api": ""
}
},
"/liquid/api/v1/ws": {
"target": "http://localhost:8999/",
"secure": false,

View File

@@ -91,6 +91,17 @@ export function app(locale: string): express.Express {
server.get('/testnet/tv', getLocalizedSSR(indexHtml));
server.get('/testnet/status', getLocalizedSSR(indexHtml));
server.get('/testnet/about', getLocalizedSSR(indexHtml));
server.get('/signet', getLocalizedSSR(indexHtml));
server.get('/signet/tx/*', getLocalizedSSR(indexHtml));
server.get('/signet/block/*', getLocalizedSSR(indexHtml));
server.get('/signet/mempool-block/*', getLocalizedSSR(indexHtml));
server.get('/signet/address/*', getLocalizedSSR(indexHtml));
server.get('/signet/blocks', getLocalizedSSR(indexHtml));
server.get('/signet/graphs', getLocalizedSSR(indexHtml));
server.get('/signet/api', getLocalizedSSR(indexHtml));
server.get('/signet/tv', getLocalizedSSR(indexHtml));
server.get('/signet/status', getLocalizedSSR(indexHtml));
server.get('/signet/about', getLocalizedSSR(indexHtml));
server.get('/bisq', getLocalizedSSR(indexHtml));
server.get('/bisq/tx/*', getLocalizedSSR(indexHtml));
server.get('/bisq/blocks', getLocalizedSSR(indexHtml));

View File

@@ -202,6 +202,68 @@ const routes: Routes = [
},
]
},
{
path: 'signet',
children: [
{
path: '',
component: MasterPageComponent,
children: [
{
path: '',
component: StartComponent,
children: [
{
path: '',
component: DashboardComponent
},
{
path: 'tx/:id',
component: TransactionComponent
},
{
path: 'block/:id',
component: BlockComponent
},
{
path: 'mempool-block/:id',
component: MempoolBlockComponent
},
],
},
{
path: 'blocks',
component: LatestBlocksComponent,
},
{
path: 'graphs',
component: StatisticsComponent,
},
{
path: 'address/:id',
children: [],
component: AddressComponent
},
{
path: 'api',
component: ApiDocsComponent,
},
],
},
{
path: 'tv',
component: TelevisionComponent
},
{
path: 'status',
component: StatusViewComponent
},
{
path: '**',
redirectTo: ''
},
]
},
{
path: 'bisq',
component: MasterPageComponent,

View File

@@ -59,12 +59,12 @@ export const languages: Language[] = [
{ 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: '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: 'hu', name: 'Magyar' }, // Hungarian
// { code: 'mk', name: 'Македонски' }, // Macedonian
// { code: 'ms', name: 'Bahasa Melayu' }, // Malay
{ code: 'nl', name: 'Nederlands' }, // Dutch

View File

@@ -44,7 +44,7 @@ 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';
faLink, faList, faSearch, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faAngleDoubleUp } 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';
@@ -125,5 +125,7 @@ export class AppModule {
library.addIcons(faAngleDown);
library.addIcons(faAngleUp);
library.addIcons(faExchangeAlt);
library.addIcons(faAngleDoubleUp);
library.addIcons(faAngleDoubleDown);
}
}

View File

@@ -10,7 +10,7 @@
<tbody *ngIf="!isLoading; else loadingTemplate">
<tr>
<td class="td-width" i18n="BSQ existing amount">Existing amount</td>
<td>{{ (stats.minted - stats.burnt) / 100 | number: '1.2-2' }} BSQ</td>
<td>{{ (stats.minted - stats.burnt) | number: '1.2-2' }} BSQ</td>
</tr>
<tr>
<td i18n="BSQ minted amount">Minted amount</td>
@@ -38,7 +38,7 @@
</tr>
<tr>
<td i18n="BSQ token market cap">Market cap</td>
<td><app-fiat [value]="price * (stats.minted - stats.burnt) / 100"></app-fiat></td>
<td><app-fiat [value]="price * (stats.minted - stats.burnt)"></app-fiat></td>
</tr>
</tbody>
</table>
@@ -83,4 +83,4 @@
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</ng-template>
</ng-template>

View File

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

View File

@@ -10,7 +10,8 @@
height: 80px;
background-size: 100%, 100%;
border-radius: 50%;
margin: 10px;
margin: 25px;
line-height: 32px;
}
.profile_img {
@@ -20,6 +21,20 @@
border: 0;
}
.community_sponsor {
margin: 6px;
}
.enterprise_sponsor {
margin-left: 40px;
margin-right: 40px;
}
.project_contributor {
width: 130px;
margin: 25px auto;
}
.text-small {
font-size: 12px;
}
@@ -32,4 +47,4 @@
.required {
color: #FF0000;
font-weight: bold;
}
}

View File

@@ -1,27 +1,34 @@
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 } from 'rxjs';
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 { map } from 'rxjs/operators';
import { delay, retryWhen, switchMap, tap } from 'rxjs/operators';
import { IBackendInfo } from 'src/app/interfaces/websocket.interface';
@Component({
selector: 'app-about',
templateUrl: './about.component.html',
styleUrls: ['./about.component.scss'],
})
export class AboutComponent implements OnInit {
gitCommit$: Observable<string>;
export class AboutComponent implements OnInit, OnDestroy {
backendInfo$: Observable<IBackendInfo>;
donationForm: FormGroup;
paymentForm: FormGroup;
donationStatus = 1;
sponsors$: Observable<any>;
contributors$: Observable<any>;
donationObj: any;
sponsorsEnabled = this.stateService.env.SPONSORS_ENABLED;
sponsorsEnabled = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
frontendGitCommitHash = this.stateService.env.GIT_COMMIT_HASH.substr(0, 8);
packetJsonVersion = this.stateService.env.PACKAGE_JSON_VERSION;
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
sponsors = null;
contributors = null;
requestSubscription: Subscription | undefined;
constructor(
private websocketService: WebsocketService,
@@ -33,7 +40,7 @@ export class AboutComponent implements OnInit {
) { }
ngOnInit() {
this.gitCommit$ = this.stateService.gitCommit$.pipe(map((str) => str.substr(0, 8)));
this.backendInfo$ = this.stateService.backendInfo$;
this.seoService.setTitle($localize`:@@004b222ff9ef9dd4771b777950ca1d0e4cd4348a:About`);
this.websocketService.want(['blocks']);
@@ -51,22 +58,41 @@ export class AboutComponent implements OnInit {
this.sponsors = sponsors;
});
this.apiService.getDonation$()
this.stateService.donationConfirmed$.subscribe(() => this.donationStatus = 4);
this.apiService.getContributor$()
.subscribe((contributors) => {
this.contributors = contributors;
});
}
ngOnDestroy() {
if (this.requestSubscription) {
this.requestSubscription.unsubscribe();
}
}
submitDonation() {
if (this.donationForm.invalid) {
return;
}
this.apiService.requestDonation$(
this.requestSubscription = this.apiService.requestDonation$(
this.donationForm.get('amount').value,
this.donationForm.get('handle').value
)
.subscribe((response) => {
this.websocketService.trackDonation(response.id);
this.donationObj = response;
this.donationStatus = 3;
.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 });
}
});
}

View File

@@ -1,2 +1,17 @@
<span *ngIf="multisig" class="badge badge-pill badge-warning" i18n="address-labels.multisig">multisig {{ multisigM }} of {{ multisigN }}</span>
<span *ngIf="secondLayerClose" class="badge badge-pill badge-warning" i18n="address-labels.upper-layer-peg-out">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 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,14 +16,16 @@
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<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 [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 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>
@@ -70,7 +72,7 @@
<ng-container *ngIf="addressLoadingStatus$ | async as addressLoadingStatus">
<br>
<div class="progress position-relative progress-dark">
<div class="progress progress-dark">
<div class="progress-bar progress-darklight" role="progressbar" [ngStyle]="{'width': addressLoadingStatus + '%' }"></div>
</div>
</ng-container>
@@ -115,7 +117,7 @@
<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:
Consider viewing 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>

View File

@@ -8,6 +8,7 @@
<ng-template #default>
&lrm;{{ satoshis / 100000000 | number : digitsInfo }}
<ng-template [ngIf]="network === 'liquid'">L-</ng-template>
<ng-template [ngIf]="network === 'testnet'">t</ng-template>BTC
<ng-template [ngIf]="network === 'testnet'">t</ng-template>
<ng-template [ngIf]="network === 'signet'">s</ng-template>BTC
</ng-template>
</ng-template>

View File

@@ -40,6 +40,10 @@
<td class="nowrap"><a href="{{ network.val === '' ? '' : '/' + network.val }}/api/v1/fees/mempool-blocks" target="_blank">GET {{ network.val === '' ? '' : '/' + network.val }}/api/v1/fees/mempool-blocks</a></td>
<td i18n="api-docs.fees.mempool-blocks|API Docs for /api/v1/fees/mempool-blocks">Returns current mempool as projected blocks.</td>
</tr>
<tr>
<td class="nowrap"><a href="{{ network.val === '' ? '' : '/' + network.val }}/api/v1/cpfp/TXID" target="_blank">GET {{ network.val === '' ? '' : '/' + network.val }}/api/v1/cpfp/:txid</a></td>
<td i18n="api-docs.fees.cpfp|API Docs for /api/v1/fees/cpfp">Returns the ancestors and the best descendant fees for a transaction.</td>
</tr>
</table>
</ng-template>
@@ -101,7 +105,7 @@
<td i18n>Returns the transaction at index <code>:index</code> within the specified block.</td>
</tr>
<tr>
<td class="nowrap"><a href="{{ network.val === '' ? '' : '/' + network.val }}/api/block/000000000000000015dc777b3ff2611091336355d3f0ee9766a2cf3be8e4b1ce/raw" target="_blank">GET {{ network.val === '' ? '' : '/' + network.val }}/api/block/:hash/txid/raw</a></td>
<td class="nowrap"><a href="{{ network.val === '' ? '' : '/' + network.val }}/api/block/000000000000000015dc777b3ff2611091336355d3f0ee9766a2cf3be8e4b1ce/raw" target="_blank">GET {{ network.val === '' ? '' : '/' + network.val }}/api/block/:hash/raw</a></td>
<td i18n>Returns the raw block representation in binary.</td>
</tr>
<tr>

View File

@@ -19,7 +19,7 @@ export class AppComponent implements OnInit {
private location: Location,
@Inject(LOCALE_ID) private locale: string,
) {
if (this.locale.startsWith('ar') || this.locale.startsWith('fa')) {
if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
this.dir = 'rtl';
this.class = 'rtl-layout';
}

View File

@@ -115,7 +115,7 @@
<ng-container *ngIf="(txsLoadingStatus$ | async) as txsLoadingStatus">
<br>
<div class="progress position-relative progress-dark">
<div class="progress progress-dark">
<div class="progress-bar progress-darklight" role="progressbar" [ngStyle]="{'width': txsLoadingStatus + '%' }"></div>
</div>
</ng-container>

View File

@@ -11,6 +11,10 @@
z-index: 10;
}
.blockLink:hover {
text-decoration: none;
}
.mined-block {
position: absolute;
top: 0px;

View File

@@ -32,6 +32,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
bisq: ['#9339f4', '#105fb0'],
liquid: ['#116761', '#183550'],
testnet: ['#1d486f', '#183550'],
signet: ['#6f1d5d', '#471850'],
};
constructor(

View File

@@ -1,18 +1,18 @@
<table style="width: 100%;">
<tr *ngIf="(isLoadingWebSocket$ | async) === false && (feeEstimations$ | async) as feeEstimations; else loadingFees">
<td class="d-none d-md-block">
<td class="d-none d-md-table-cell" style="width: 33%;">
<h5 class="card-title" i18n="fees-box.low-priority">Low priority</h5>
<p class="card-text">
{{ feeEstimations.hourFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span> (<app-fiat [value]="feeEstimations.hourFee * 140" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat>)
</p>
</td>
<td>
<td style="width: 33%;">
<h5 class="card-title" i18n="fees-box.medium-priority">Medium priority</h5>
<p class="card-text">
{{ feeEstimations.halfHourFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span> (<app-fiat [value]="feeEstimations.halfHourFee * 140" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat>)
</p>
</td>
<td>
<td style="width: 33%;">
<h5 class="card-title" i18n="fees-box.high-priority">High priority</h5>
<p class="card-text">
{{ feeEstimations.fastestFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span> (<app-fiat [value]="feeEstimations.fastestFee * 140" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat>)
@@ -23,15 +23,15 @@
<ng-template #loadingFees>
<tr>
<td class="d-none d-md-block">
<td class="d-none d-md-table-cell" style="width: 33%;">
<h5 class="card-title" i18n="fees-box.low-priority">Low priority</h5>
<p class="card-text"><span class="skeleton-loader"></span></p>
</td>
<td>
<td style="width: 33%;">
<h5 class="card-title" i18n="fees-box.medium-priority">Medium priority</h5>
<p class="card-text"><span class="skeleton-loader" style="width: 80%;"></span></p>
</td>
<td>
<td style="width: 33%;">
<h5 class="card-title" i18n="fees-box.high-priority">High priority</h5>
<p class="card-text"><span class="skeleton-loader"></span></p>
</td>

View File

@@ -40,9 +40,9 @@ export class FeesBoxComponent implements OnInit {
};
}
const firstMedianFee = this.optimizeMedianFee(pBlocks[0]);
const secondMedianFee = pBlocks[1] ? this.optimizeMedianFee(pBlocks[1], firstMedianFee) : this.defaultFee;
const thirdMedianFee = pBlocks[2] ? this.optimizeMedianFee(pBlocks[2], secondMedianFee) : this.defaultFee;
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,
@@ -53,12 +53,12 @@ export class FeesBoxComponent implements OnInit {
);
}
optimizeMedianFee(pBlock: MempoolBlock, previousFee?: number): number {
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) {
if (pBlock.blockVSize <= 950000 && !nextBlock) {
const multiplier = (pBlock.blockVSize - 500000) / 500000;
return Math.max(Math.round(useFee * multiplier), this.defaultFee);
}

View File

@@ -19,7 +19,7 @@
<td><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></td>
<td class="d-none d-lg-block">{{ block.tx_count | number }}</td>
<td>
<div class="progress position-relative">
<div class="progress">
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / 4000000)*100 + '%' }"></div>
<div class="progress-text">{{ block.size | bytes: 2 }}</div>
</div>
@@ -36,7 +36,7 @@
<ng-container *ngIf="(blocksLoadingStatus$ | async) as blocksLoadingStatus">
<tr>
<td colspan="5">
<div class="progress position-relative progress-dark">
<div class="progress progress-dark">
<div class="progress-bar progress-darklight" role="progressbar" [ngStyle]="{'width': blocksLoadingStatus + '%' }"></div>
</div>
</td>

View File

@@ -3,18 +3,21 @@
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
<img src="./resources/mempool-logo.png" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }">
<div class="badge badge-warning connection-badge" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
<div class="badge badge-warning connection-badge" style="left: 0px;" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>
<img [src]="officialMempoolSpace ? './resources/mempool-space-logo.png' : './resources/mempool-logo.png'" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }">
<div class="connection-badge">
<div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>
</div>
</ng-container>
</a>
<div ngbDropdown display="dynamic" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED">
<div ngbDropdown display="dynamic" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED">
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
<img src="./resources/{{ network.val === '' ? 'bitcoin' : network.val }}-logo.png" style="width: 25px; height: 25px;" class="mr-1">
</button>
<div ngbDropdownMenu>
<button ngbDropdownItem class="mainnet" routerLink="/"><img src="./resources/bitcoin-logo.png" style="width: 30px;" class="mr-1"> Mainnet</button>
<button ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" routerLink="/signet"><img src="./resources/signet-logo.png" style="width: 30px;" class="mr-1"> Signet</button>
<button ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" routerLink="/testnet"><img src="./resources/testnet-logo.png" style="width: 30px;" class="mr-1"> Testnet</button>
<h6 *ngIf="env.LIQUID_ENABLED || env.BISQ_ENABLED" class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
<button ngbDropdownItem *ngIf="env.BISQ_ENABLED" class="mainnet" [class.active]="network.val === 'bisq'" routerLink="/bisq"><img src="./resources/bisq-logo.png" style="width: 30px;" class="mr-1"> Bisq</button>

View File

@@ -44,10 +44,15 @@ nav {
}
.connection-badge {
margin-left: 10px;
position: absolute;
top: 13px;
left: 40px;
left: 0px;
width: 140px;
}
.badge {
margin: 0 auto;
display: table;
}
.mainnet.active {
@@ -62,6 +67,10 @@ nav {
background-color: #1d486f;
}
.signet.active {
background-color: #6f1d5d;
}
.dropdown-divider {
border-top: 1px solid #121420;
}

View File

@@ -13,6 +13,7 @@ export class MasterPageComponent implements OnInit {
connectionState$: Observable<number>;
navCollapsed = false;
isMobile = window.innerWidth <= 767.98;
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
constructor(
private stateService: StateService,

View File

@@ -31,7 +31,7 @@
<tr>
<td i18n="mempool-block.size">Size</td>
<td>
<div class="progress position-relative">
<div class="progress">
<div class="progress-bar progress-mempool {{ (network$ | async) }}" role="progressbar" [ngStyle]="{'width': (mempoolBlock.blockVSize / 1000000) * 100 + '%' }"></div>
<div class="progress-text">{{ mempoolBlock.blockSize | bytes: 2 }}</div>
</div>

View File

@@ -116,3 +116,7 @@
left: 0;
z-index: 10;
}
.blockLink:hover {
text-decoration: none;
}

View File

@@ -32,7 +32,6 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
) { }
ngOnInit(): void {
const showLegend = !this.isMobile && this.showLegend;
let labelHops = !this.showLegend ? 48 : 24;
if (this.small) {
labelHops = labelHops / 2;
@@ -73,28 +72,28 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
},
axisY: {
labelInterpolationFnc: (value: number): any => this.vbytesPipe.transform(value, 2),
offset: showLegend ? 160 : 60,
offset: this.showLegend ? 160 : 60,
},
plugins: this.inverted ? [Chartist.plugins.ctTargetLine({ value: 1000000 })] : []
};
if (showLegend) {
this.mempoolVsizeFeesOptions.plugins.push(
Chartist.plugins.legend({
legendNames: [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
if (this.showLegend) {
const legendNames: string[] = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
250, 300, 350, 400].map((sat, i, arr) => {
if (sat === 400) {
return '350+';
return '350+';
}
if (i === 0) {
if (this.stateService.network === 'liquid') {
return '0 - 1';
}
return '1 sat/vB';
return '0 - 1';
}
return arr[i - 1] + ' - ' + sat;
})
})
});
// Only Liquid has lower than 1 sat/vb transactions
if (this.stateService.network !== 'liquid') {
legendNames.shift();
}
this.mempoolVsizeFeesOptions.plugins.push(
Chartist.plugins.legend({ legendNames: legendNames })
);
}
}

View File

@@ -309,7 +309,7 @@ const defaultOptions = {
Chartist.plugins.legend = function (options: any) {
let cachedDOMPosition;
let cacheInactiveLegends: any = [];
let cacheInactiveLegends: { [key:number]: boolean } = {};
// Catch invalid options
if (options && options.position) {
if (!(options.position === 'top' || options.position === 'bottom' || options.position instanceof HTMLElement)) {
@@ -449,32 +449,29 @@ Chartist.plugins.legend = function (options: any) {
const legendIndex = parseInt(li.getAttribute('data-legend'));
const legend = legends[legendIndex];
if (!legend.active) {
legend.active = true;
li.classList.remove('inactive');
const activateLegend = (_legendIndex: number): void => {
legends[_legendIndex].active = true;
legendElement.childNodes[_legendIndex].classList.remove('inactive');
var indexOfInactiveLegend = cacheInactiveLegends.indexOf(legendIndex, 0)
if (indexOfInactiveLegend > -1) {
cacheInactiveLegends.splice(indexOfInactiveLegend, 1);
}
cacheInactiveLegends[_legendIndex] = false;
}
} else {
legend.active = false;
li.classList.add('inactive');
cacheInactiveLegends.push(legendIndex);
const deactivateLegend = (_legendIndex: number): void => {
legends[_legendIndex].active = false;
legendElement.childNodes[_legendIndex].classList.add('inactive');
cacheInactiveLegends[_legendIndex] = true;
}
const activeCount = legends.filter(function(legend: any) { return legend.active; }).length;
if (!options.removeAll && activeCount == 0) {
// If we can't disable all series at the same time, let's
// reenable all of them:
for (let i = 0; i < legends.length; i++) {
legends[i].active = true;
legendElement.childNodes[i].classList.remove('inactive');
}
cacheInactiveLegends = [];
for (let i = legends.length - 1; i >= 0; i--) {
if (i >= legendIndex) {
if (!legend.active) activateLegend(i);
} else {
if (legend.active) deactivateLegend(i);
}
}
// Make sure all values are undefined (falsy) when clicking the first legend
// After clicking the first legend all indices should be falsy
if (legendIndex === 0) cacheInactiveLegends = {};
const newSeries = [];
const newLabels = [];
@@ -512,7 +509,8 @@ Chartist.plugins.legend = function (options: any) {
const legendSeries = legend.series || [i];
const li = createNameElement(i, legendText, classNamesViable);
const isActive: boolean = !(cacheInactiveLegends.indexOf(i) > -1);
// If the value is undefined or false, isActive is true
const isActive: boolean = !cacheInactiveLegends[i];
if (isActive) {
activeSeries.push(seriesMetadata[i].data);
activeLabels.push(seriesMetadata[i].label);

View File

@@ -1,16 +1,19 @@
<div class="container-xl">
<div class="text-center">
<br />
<img src="./resources/mempool-logo-bigger.png" height="67.5" width="251">
<img [src]="officialMempoolSpace ? './resources/mempool-space-logo-bigger.png' : './resources/mempool-logo-bigger.png'" style="width: 250px;height:63px;">
<br /><br />
<h2>Terms of Service</h2>
<h6>Updated: October 12, 2020</h6>
<h6>Updated: March 09, 2021</h6>
<br><br>
<div class="text-left">
<p *ngIf="officialMempoolSpace">The official <a href="https://mempool.space/">mempool.space</a> website and <a href="https://mempool.space/api">its API service</a> are operated by Mempool Space K.K.</p>
<p *ngIf="!officialMempoolSpace">This website and its API service are operated by an individual member of the Bitcoin community, who is not affiliated with Mempool Space K.K.</p>
<h5>By using this website or accessing its API, you agree to these Terms of Service:</h5>
<br>
@@ -29,16 +32,44 @@
<h4>PRIVACY POLICY</h4>
<p>The operators of this website may collect your IP address as part of basic webserver logs and a self-hosted statistics application, but this data will never be sold or shared with third parties. Please use Tor when accessing this website to protect your privacy.</p>
<p>Out of respect for the Bitcoin community, this website does not utilize any third-party trackers or cookies, and the operators of this website do not share any user data with third-parties. However, your IP address may be collected as part of basic webserver logs necessary for systems administration purposes, and as part of an anonymized self-hosted statistics application for analytics. To protect your privacy, use Tor browser to access this website's Tor V3 onion hostname to conceal your IP address, or alternatively install and use your own self-hosted instance of this website.</p>
<h4>SPONSOR POLICY</h4>
<br>
<p>If you donate at least 0.01 BTC to mempool.space and submit your Twitter username at the time of donation, we will generally feature your Twitter profile photo on our About page with a link to your Twitter account. However, we reserve the right to remove sponsors listed on our website that in our sole discretion are inappropriate. All donations made are non-refundable.</p>
<h4>BITCOIN ONLY POLICY</h4>
</div>
<p>Out of respect for the Bitcoin community, this website does not support altcoins, and is generally "Bitcoin Only". The <a href="https://mempool.space/liquid/assets">IOU tokens for Bitcoin and fiat currencies on the Liquid network</a>, and the <a href="https://mempool.space/bisq/stats">BSQ colored Bitcoin for DAO governance on the Bisq Network</a>, are not considered altcoins as they do not distract users from Bitcoin and are part of our stated mission to support the entire Bitcoin ecosystem including Bitcoin Layer 2 Networks.</p>
<br><br>
<br>
<h4>EXTERNAL LINK POLICY</h4>
<p>Out of respect for the Bitcoin community, this website does not display advertising, and generally does not link to external websites. However, exceptions are made for references to Bitcoin's technical documentation, for acknowledgements of individual members and open-source projects within the Bitcoin community, and for direct contributors and supporters of <a href="https://github.com/mempool/mempool">The Mempool Open Source Project</a>, as follows:</p>
<ul>
<li>In order to refer to Bitcoin technical documentation, external links may by added where applicable, to informational websites that respect that Bitcoin community and do not display advertising, altcoins, or third-party trackers, such as <a href="https://en.bitcoin.it/">bitcoin.it</a> and <a href="https://bitcoin.org/">bitcoin.org</a>.</li>
<br>
<li>In order to acknowledge individual contributors to <a href="https://github.com/mempool/mempool">The Mempool Open Source Project</a> who have made <a href="https://github.com/mempool/mempool/graphs/contributors">at least 10 contributions visible on GitHub</a>, the contributor's GitHub profile photo will automatically be displayed on <a href="https://mempool.space/about">mempool.space/about</a>, with <u>a link to their GitHub profile</u>.</li>
<br>
<li>In order to acknowledge members of the Bitcoin community who have made a one-time donation of at least 0.01 BTC to <a href="https://github.com/mempool/mempool">The Mempool Open Source Project</a> on <a href="https://mempool.space/about">mempool.space/about</a>, the Bitcoin community members's Twitter profile photo will automatically be displayed on <a href="https://mempool.space/about">mempool.space/about</a>, with <u>a link to their Twitter profile</u>.</li>
<br>
<li>In order to acknowledge members of the Bitcoin community who have made recurring donations of at least $100 USD per month to <a href="https://github.com/mempool/mempool">The Mempool Open Source Project</a> through <a href="https://github.com/sponsors/mempool">our GitHub Sponsors program</a> for at least 6 months, the Bitcoin community member's GitHub profile photo will automatically be displayed on <a href="https://mempool.space/about">mempool.space/about</a>, with <u>a link to their GitHub profile</u>.</li>
<br>
<li>In order to acknowledge open source projects that benefit the Bitcoin community and have integrated <a href="https://github.com/mempool/mempool">The Mempool Open Source Project</a> into their software, the open source project's logo may be displayed on <a href="https://mempool.space/about">mempool.space/about</a>, with <u>a link to the project's GitHub repository</u>.</li>
<br>
<li>In order to acknowledge federations or alliances within the Bitcoin community that have a relationship to <a href="https://github.com/mempool/mempool">The Mempool Open Source Project</a>, the federation or alliance logo may be displayed on <a href="https://mempool.space/about">mempool.space/about</a>, with <u>a link to the alliance website</u>.</li>
<br>
<li>In order to acknowledge the enterprise organizations within the Bitcoin community that have made a significant one-time donation of at least $25,000 USD to <a href="https://github.com/mempool/mempool">The Mempool Open Source Project</a>, the organization's logo may be displayed on <a href="https://mempool.space/about">mempool.space/about</a>, with <u>a link to the organization's website</u>.</li>
</ul>
<p>However, we reserve the right to remove any links from our website that in our sole discretion are inappropriate. All donations are non-refundable.</p>
<br>
<p>EOF</p>
</div>
</div>
<br><br>
</div>

View File

@@ -1,9 +1,14 @@
import { Component } from '@angular/core';
import { Env, StateService } from '../../services/state.service';
@Component({
selector: 'app-terms-of-service',
templateUrl: './terms-of-service.component.html'
})
export class TermsOfServiceComponent {
constructor() { }
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
constructor(
private stateService: StateService,
) { }
}

View File

@@ -2,7 +2,7 @@
<div class="title-block">
<div *ngIf="rbfTransaction" class="alert alert-mempool" role="alert">
<span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
<span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>&nbsp;
<a class="alert-link" [routerLink]="['/tx/' | relativeUrl, rbfTransaction.txid]" [state]="{ data: rbfTransaction }">
<span class="d-inline d-lg-none">{{ rbfTransaction.txid | shortenString : 24 }}</span>
<span class="d-none d-lg-inline">{{ rbfTransaction.txid }}</span>
@@ -73,22 +73,7 @@
</table>
</div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
<td>{{ tx.fee | number }} <span i18n="transaction.fee.sat|Transaction Fee sat">sat</span> (<app-fiat [value]="tx.fee"></app-fiat>)</td>
</tr>
<tr>
<td i18n="transaction.fee-per-vbyte|Transaction fee">Fee per vByte</td>
<td>
{{ tx.fee / (tx.weight / 4) | number : '1.1-1' }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
&nbsp;
<app-tx-fee-rating *ngIf="tx.fee" [tx]="tx"></app-tx-fee-rating>
</td>
</tr>
</tbody>
</table>
<ng-container *ngTemplateOutlet="feeTable"></ng-container>
</div>
</div>
@@ -146,23 +131,60 @@
</table>
</div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="transaction.fee|Transaction Fee">Fee</td>
<td>{{ tx.fee | number }} <span i18n="transaction.fee.sat|Transaction Fee sat">sat</span> (<app-fiat [value]="tx.fee"></app-fiat>)</td>
</tr>
<tr>
<td i18n="transaction.fee-per-vbyte|Transaction fee">Fee per vByte</td>
<td>{{ tx.fee / (tx.weight / 4) | number : '1.1-1' }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
</tr>
</tbody>
</table>
<ng-container *ngTemplateOutlet="feeTable"></ng-container>
</div>
</div>
</div>
</ng-template>
<ng-template [ngIf]="showCpfpDetails">
<br>
<h2 class="text-left">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="xs"></fa-icon></h2>
<div class="box">
<table class="table table-borderless table-striped">
<thead>
<tr>
<th i18n="transactions-list.vout.scriptpubkey-type">Type</th>
<th i18n="dashboard.latest-transactions.txid">TXID</th>
<th class="d-none d-lg-table-cell" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</th>
<th i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th>
<th class="d-none d-lg-table-cell"></th>
</tr>
</thead>
<tbody>
<ng-template [ngIf]="cpfpInfo.bestDescendant">
<tr>
<td><span class="badge badge-success" i18n="transaction.descendant|Descendant">Descendant</span></td>
<td>
<a [routerLink]="['/tx' | relativeUrl, cpfpInfo.bestDescendant.txid]">
<span class="d-inline d-lg-none">{{ cpfpInfo.bestDescendant.txid | shortenString : 10 }}</span>
<span class="d-none d-lg-inline">{{ cpfpInfo.bestDescendant.txid }}</span>
</a>
</td>
<td class="d-none d-lg-table-cell">{{ cpfpInfo.bestDescendant.weight / 4 | vbytes: 2 }}</td>
<td>{{ cpfpInfo.bestDescendant.fee / (cpfpInfo.bestDescendant.weight / 4) | number : '1.1-1' }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
<td class="d-none d-lg-table-cell"><fa-icon class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
</tr>
</ng-template>
<ng-template [ngIf]="cpfpInfo.ancestors.length">
<tr *ngFor="let cpfpTx of cpfpInfo.ancestors">
<td><span class="badge badge-primary" i18n="transaction.ancestor|Transaction Ancestor">Ancestor</span></td>
<td><a [routerLink]="['/tx' | relativeUrl, cpfpTx.txid]">
<span class="d-inline d-lg-none">{{ cpfpTx.txid | shortenString : 10 }}</span>
<span class="d-none d-lg-inline">{{ cpfpTx.txid }}</span>
</a>
</td>
<td class="d-none d-lg-table-cell">{{ cpfpTx.weight / 4 | vbytes: 2 }}</td>
<td>{{ cpfpTx.fee / (cpfpTx.weight / 4) | number : '1.1-1' }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
<td class="d-none d-lg-table-cell"><fa-icon *ngIf="roundToOneDecimal(cpfpTx) < roundToOneDecimal(tx)" class="arrow-red" [icon]="['fas', 'angle-double-down']" [fixedWidth]="true"></fa-icon></td>
</tr>
</ng-template>
</tbody>
</table>
</div>
</ng-template>
<br>
<h2 class="float-left" i18n="transaction.inputs-and-outputs|Transaction inputs and outputs">Inputs & Outputs</h2>
@@ -295,4 +317,35 @@
<ng-template let-i #nextBlockEta i18n="mempool-blocks.eta-of-next-block|Block Frequency">In ~{{ i }} minute</ng-template>
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} block</ng-template>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} blocks</ng-template>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} blocks</ng-template>
<ng-template #feeTable>
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
<td>{{ tx.fee | number }} <span i18n="transaction.fee.sat|Transaction Fee sat">sat</span> (<app-fiat [value]="tx.fee"></app-fiat>)</td>
</tr>
<tr>
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
<td>
{{ tx.feePerVsize | number : '1.1-1' }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
<ng-template [ngIf]="tx.status.confirmed">
&nbsp;
<app-tx-fee-rating *ngIf="tx.fee && (cpfpInfo && !cpfpInfo.bestDescendant && !cpfpInfo.ancestors.length)" [tx]="tx"></app-tx-fee-rating>
</ng-template>
</td>
</tr>
<tr *ngIf="cpfpInfo && (cpfpInfo.bestDescendant || cpfpInfo.ancestors.length)">
<td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
<td>
{{ tx.effectiveFeePerVsize | number : '1.1-1' }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
<ng-template [ngIf]="tx.status.confirmed">
<app-tx-fee-rating class="d-none d-lg-inline ml-2" *ngIf="tx.fee" [tx]="tx"></app-tx-fee-rating>
</ng-template>
<button type="button" class="btn btn-outline-info btn-sm btn-small-height ml-2 float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
</td>
</tr>
</tbody>
</table>
</ng-template>

View File

@@ -26,4 +26,21 @@
h1 {
margin-bottom: 0;
}
.badge {
position: relative;
top: -1px;
}
.btn-small-height {
line-height: 1.1;
}
.arrow-green {
color: #1a9436;
}
.arrow-red {
color: #dc3545;
}

View File

@@ -9,6 +9,7 @@ import { WebsocketService } from '../../services/websocket.service';
import { AudioService } from 'src/app/services/audio.service';
import { ApiService } from 'src/app/services/api.service';
import { SeoService } from 'src/app/services/seo.service';
import { CpfpInfo } from 'src/app/interfaces/node-api.interface';
@Component({
selector: 'app-transaction',
@@ -27,6 +28,8 @@ export class TransactionComponent implements OnInit, OnDestroy {
transactionTime = -1;
subscription: Subscription;
rbfTransaction: undefined | Transaction;
cpfpInfo: CpfpInfo | null;
showCpfpDetails = false;
constructor(
private route: ActivatedRoute,
@@ -77,6 +80,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
if (tx.fee === undefined) {
this.tx.fee = 0;
}
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
this.isLoadingTx = false;
this.error = undefined;
this.waitingForTransaction = false;
@@ -91,10 +95,33 @@ export class TransactionComponent implements OnInit, OnDestroy {
this.getTransactionTime();
}
}
if (this.tx.status.confirmed) {
this.stateService.markBlock$.next({ blockHeight: tx.status.block_height });
} else {
this.stateService.markBlock$.next({ txFeePerVSize: tx.fee / (tx.weight / 4) });
if (tx.effectiveFeePerVsize) {
this.stateService.markBlock$.next({ txFeePerVSize: tx.effectiveFeePerVsize });
this.cpfpInfo = {
ancestors: tx.ancestors,
bestDescendant: tx.bestDescendant,
};
} else {
this.apiService.getCpfpinfo$(this.tx.txid)
.subscribe((cpfpInfo) => {
const lowerFeeParents = cpfpInfo.ancestors.filter((parent) => (parent.fee / (parent.weight / 4)) < tx.feePerVsize);
let totalWeight = tx.weight + lowerFeeParents.reduce((prev, val) => prev + val.weight, 0);
let totalFees = tx.fee + lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
if (cpfpInfo.bestDescendant) {
totalWeight += cpfpInfo.bestDescendant.weight;
totalFees += cpfpInfo.bestDescendant.fee;
}
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
this.stateService.markBlock$.next({ txFeePerVSize: this.tx.effectiveFeePerVsize });
this.cpfpInfo = cpfpInfo;
});
}
}
},
(error) => {
@@ -139,7 +166,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
return;
}
const txFeePerVSize = this.tx.fee / (this.tx.weight / 4);
const txFeePerVSize = this.tx.effectiveFeePerVsize || this.tx.fee / (this.tx.weight / 4);
for (const block of mempoolBlocks) {
for (let i = 0; i < block.feeRange.length - 1; i++) {
@@ -165,6 +192,8 @@ export class TransactionComponent implements OnInit, OnDestroy {
this.isLoadingTx = true;
this.rbfTransaction = undefined;
this.transactionTime = -1;
this.cpfpInfo = null;
this.showCpfpDetails = false;
document.body.scrollTo(0, 0);
this.leaveTransaction();
}
@@ -174,6 +203,10 @@ export class TransactionComponent implements OnInit, OnDestroy {
this.stateService.markBlock$.next({});
}
roundToOneDecimal(cpfpTx: any): number {
return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1);
}
ngOnDestroy() {
this.subscription.unsubscribe();
this.leaveTransaction();

View File

@@ -40,6 +40,9 @@
<ng-container *ngSwitchCase="vin.is_pegin">
<span i18n="transactions-list.peg-in">Peg-in</span>
</ng-container>
<ng-container *ngSwitchCase="vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk'">
<span>P2PK</span>
</ng-container>
<ng-container *ngSwitchDefault>
<a [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
<span class="d-block d-lg-none">{{ vin.prevout.scriptpubkey_address | shortenString : 16 }}</span>

View File

@@ -5,4 +5,5 @@
<span *ngIf="segwitGains.potentialP2shGains" class="badge badge-danger mr-1" i18n-ngbTooltip="ngbTooltip about missed out gains" ngbTooltip="This transaction could save {{ segwitGains.potentialBech32Gains * 100 | number : '1.0-0' }}% on fees by upgrading to native SegWit-Bech32 or {{ segwitGains.potentialP2shGains * 100 | number: '1.0-0' }}% by upgrading to SegWit-P2SH" placement="bottom"><del i18n="tx-features.tag.segwit|SegWit">SegWit</del></span>
</ng-template>
</ng-template>
<span *ngIf="isRbfTransaction" class="badge badge-success" i18n-ngbTooltip="RBF tooltip" ngbTooltip="This transaction support Replace-By-Fee (RBF) allowing fee bumping" placement="bottom" i18n="tx-features.tag.rbf|RBF">RBF</span>
<span *ngIf="isRbfTransaction; else rbfDisabled" class="badge badge-success" i18n-ngbTooltip="RBF tooltip" ngbTooltip="This transaction support Replace-By-Fee (RBF) allowing fee bumping" placement="bottom" i18n="tx-features.tag.rbf|RBF">RBF</span>
<ng-template #rbfDisabled><span class="badge badge-danger mr-1" i18n-ngbTooltip="RBF disabled tooltip" ngbTooltip="This transaction does NOT support Replace-By-Fee (RBF) and cannot be fee bumped using this method" placement="bottom"><del i18n="tx-features.tag.rbf|RBF">RBF</del></span></ng-template>

View File

@@ -1,3 +1,3 @@
<span *ngIf="feeRating === 1" class="badge badge-success" i18n="tx-fee-rating.optimal|TX Fee Rating is Optimal">Optimal</span>
<span *ngIf="feeRating === 2" class="badge badge-warning" placement="bottom" i18n-ngbTooltip="tx-fee-rating.warning-tooltip" ngbTooltip="Only ~{{ medianFeeNeeded }} sat/vB was needed to get into this block" i18n="tx-fee-rating.overpaid.warning|TX Fee Rating is Warning">Overpaid {{ overpaidTimes }}x</span>
<span *ngIf="feeRating === 3" class="badge badge-danger" placement="bottom" i18n-ngbTooltip="tx-fee-rating.danger-tooltip" ngbTooltip="Only ~{{ medianFeeNeeded }} sat/vB was needed to get into this block" i18n="tx-fee-rating.overpaid.danger|TX Fee Rating is Danger">Overpaid {{ overpaidTimes }}x</span>
<span *ngIf="feeRating === 2" class="badge badge-warning" placement="bottom" i18n-ngbTooltip="tx-fee-rating.warning-tooltip" ngbTooltip="Only ~{{ medianFeeNeeded | number : '1.1-1' }} sat/vB was needed to get into this block" i18n="tx-fee-rating.overpaid.warning|TX Fee Rating is Warning">Overpaid {{ overpaidTimes }}x</span>
<span *ngIf="feeRating === 3" class="badge badge-danger" placement="bottom" i18n-ngbTooltip="tx-fee-rating.warning-tooltip" ngbTooltip="Only ~{{ medianFeeNeeded | number : '1.1-1' }} sat/vB was needed to get into this block" i18n="tx-fee-rating.overpaid.warning|TX Fee Rating is Warning">Overpaid {{ overpaidTimes }}x</span>

View File

@@ -28,7 +28,7 @@ export class TxFeeRatingComponent implements OnInit, OnChanges, OnDestroy {
ngOnInit() {
this.blocksSubscription = this.stateService.blocks$.subscribe(([block]) => {
this.blocks.push(block);
if (this.tx.status.confirmed && this.tx.status.block_height === block.height) {
if (this.tx.status.confirmed && this.tx.status.block_height === block.height && block.medianFee > 0) {
this.calculateRatings(block);
this.cd.markForCheck();
}
@@ -42,7 +42,7 @@ export class TxFeeRatingComponent implements OnInit, OnChanges, OnDestroy {
}
const foundBlock = this.blocks.find((b) => b.height === this.tx.status.block_height);
if (foundBlock) {
if (foundBlock && foundBlock.medianFee > 0) {
this.calculateRatings(foundBlock);
}
}
@@ -52,8 +52,8 @@ export class TxFeeRatingComponent implements OnInit, OnChanges, OnDestroy {
}
calculateRatings(block: Block) {
const feePervByte = this.tx.fee / (this.tx.weight / 4);
this.medianFeeNeeded = Math.round(block.feeRange[Math.round(block.feeRange.length * 0.5)]);
const feePervByte = this.tx.effectiveFeePerVsize || this.tx.fee / (this.tx.weight / 4);
this.medianFeeNeeded = block.medianFee;
// Block not filled
if (block.weight < 4000000 * 0.95) {

View File

@@ -86,8 +86,8 @@
<td><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></td>
<td class="d-none d-lg-table-cell">{{ block.tx_count | number }}</td>
<td>
<div class="progress position-relative">
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / 4000000)*100 + '%' }"></div>
<div class="progress">
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / 4000000)*100 + '%' }">&nbsp;</div>
<div class="progress-text">{{ block.size | bytes: 2 }}</div>
</div>
</td>
@@ -114,7 +114,7 @@
<td><a [routerLink]="['/tx' | relativeUrl, transaction.txid]">{{ transaction.txid | shortenString : 10 }}</a></td>
<td class="text-right d-none d-lg-table-cell"><app-amount [satoshis]="transaction.value" digitsInfo="1.8-8" [noFiat]="true"></app-amount></td>
<td *ngIf="(network$ | async) === ''" class="text-right d-none d-lg-table-cell"><app-fiat [value]="transaction.value" digitsInfo="1.0-0"></app-fiat></td>
<td class="text-right">{{ transaction.fee / (transaction.weight / 4) | number : '1.1-1' }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
<td class="text-right">{{ transaction.fee / transaction.vsize | number : '1.1-1' }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
</tr>
</tbody>
</table>
@@ -134,7 +134,7 @@
</button>
<div [formGroup]="languageForm" class="text-small text-center mt-4">
<select formControlName="language" class="form-control form-control-secondary form-control-sm mx-auto" style="width: 130px;" (change)="changeLanguage()">
<select formControlName="language" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 130px;" (change)="changeLanguage()">
<option *ngFor="let lang of languages" [value]="lang.code">{{ lang.name }}</option>
</select>
</div>
@@ -149,6 +149,10 @@
<div class="skeleton-loader"></div>
</ng-template>
<ng-template #loadingsmall>
<span class="skeleton-loader" style="width: 80%;"></span>
</ng-template>
<ng-template #emptyBlock>
<div class="col mb-4">
@@ -158,15 +162,23 @@
<ng-template #mempoolTable let-mempoolInfoData>
<table style="width: 100%;">
<tr>
<td>
<h5 class="card-title" i18n="dashboard.mempool-size|Mempool size">Mempool size</h5>
<p class="card-text" *ngIf="(mempoolBlocksData$ | async) as mempoolBlocksData; else loading">
{{ mempoolBlocksData.size | bytes }} (<ng-container *ngTemplateOutlet="mempoolBlocksData.blocks === 1 ? blocksSingular : blocksPlural; context: {$implicit: mempoolBlocksData.blocks }"></ng-container>)
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} block</ng-template>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} blocks</ng-template>
<td style="width: 33%;">
<h5 *ngIf="!mempoolInfoData.value || mempoolInfoData.value.memPoolInfo.mempoolminfee === 0.00001 else purgingText" class="card-title" i18n="dashboard.minimum-fee|Minimum mempool fee">Minimum fee</h5>
<ng-template #purgingText><h5 class="card-title" i18n="dashboard.purging|Purgin below fee">Purging</h5></ng-template>
<p class="card-text" *ngIf="mempoolInfoData.value; else loading">
<ng-template [ngIf]="mempoolInfoData.value.memPoolInfo.mempoolminfee > 0.00001">&lt; </ng-template>{{ mempoolInfoData.value.memPoolInfo.mempoolminfee * 100000 | number : '1.1-1' }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
</p>
</td>
<td>
<td style="width: 33%;">
<h5 class="card-title" i18n="dashboard.memory-usage|Memory usage">Memory usage</h5>
<div class="card-text" *ngIf="mempoolInfoData.value; else loadingsmall">
<div class="progress" style="max-width: 250px;">
<div class="progress-bar {{ mempoolInfoData.value.mempoolSizeProgress }}" style="padding: 4px;" role="progressbar" [ngStyle]="{'width': (mempoolInfoData.value.memPoolInfo.usage / mempoolInfoData.value.memPoolInfo.maxmempool * 100) + '%' }">&nbsp;</div>
<div class="progress-text">{{ mempoolInfoData.value.memPoolInfo.usage | bytes }} / {{ mempoolInfoData.value.memPoolInfo.maxmempool | bytes }}</div>
</div>
</div>
</td>
<td class="d-none d-md-table-cell" style="width: 33%;">
<h5 class="card-title" i18n="dashboard.unconfirmed|Unconfirmed count">Unconfirmed</h5>
<p class="card-text" *ngIf="mempoolInfoData.value; else loading">
{{ mempoolInfoData.value.memPoolInfo.size | number }} <span i18n="dashboard.txs">TXs</span>
@@ -183,8 +195,9 @@
&nbsp;<span class="badge badge-pill badge-warning"><ng-container i18n="dashboard.backend-is-synchronizing">Backend is synchronizing</ng-container> ({{ mempoolLoadingStatus$ | async }}%)</span>
</span>
<ng-template #inSync>
<div class="progress sub-text" style="max-width: 250px;">
<div class="progress-bar {{ mempoolInfoData.value.progressClass }}" style="padding: 4px;" role="progressbar" [ngStyle]="{'width': mempoolInfoData.value.progressWidth}">{{ mempoolInfoData.value.vBytesPerSecond | ceil | number }} <ng-container i18n="shared.vbytes-per-second|vB/s">vB/s</ng-container></div>
<div class="progress" style="max-width: 250px;">
<div class="progress-bar {{ mempoolInfoData.value.progressClass }}" style="padding: 4px;" role="progressbar" [ngStyle]="{'width': mempoolInfoData.value.progressWidth}">&nbsp;</div>
<div class="progress-text">{{ mempoolInfoData.value.vBytesPerSecond | ceil | number }} <ng-container i18n="shared.vbytes-per-second|vB/s">vB/s</ng-container></div>
</div>
</ng-template>
</ng-template>
@@ -195,9 +208,10 @@
<div class="card-body more-padding">
<h5 class="card-title" i18n="dashboard.difficulty-adjustment">Difficulty adjustment</h5>
<div class="progress" *ngIf="(difficultyEpoch$ | async) as epochData; else loading">
<div class="progress-bar" role="progressbar" style="width: 15%; background-color: #105fb0" [ngStyle]="{'width': epochData.base}"><ng-template [ngIf]="epochData.change > 0">+</ng-template>{{ epochData.change | number: '1.0-2' }}%</div>
<div class="progress-bar" role="progressbar" style="width: 15%; background-color: #105fb0" [ngStyle]="{'width': epochData.base}">&nbsp;</div>
<div class="progress-bar bg-success" role="progressbar" style="width: 0%" [ngStyle]="{'width': epochData.green}"></div>
<div class="progress-bar bg-danger" role="progressbar" style="width: 1%; background-color: #f14d80;" [ngStyle]="{'width': epochData.red}"></div>
<div class="progress-text"><ng-template [ngIf]="epochData.change > 0">+</ng-template>{{ epochData.change | number: '1.0-2' }}%</div>
</div>
</div>
</div>

View File

@@ -102,11 +102,20 @@ export class DashboardComponent implements OnInit {
progressClass = 'bg-warning';
}
let mempoolSizePercentage = (mempoolInfo.usage / mempoolInfo.maxmempool * 100)
let mempoolSizeProgress = 'bg-danger';
if (mempoolSizePercentage <= 50) {
mempoolSizeProgress = 'bg-success';
} else if (mempoolSizePercentage <= 75) {
mempoolSizeProgress = 'bg-warning';
}
return {
memPoolInfo: mempoolInfo,
vBytesPerSecond: vbytesPerSecond,
progressWidth: percent + '%',
progressClass: progressClass,
mempoolSizeProgress: mempoolSizeProgress,
};
})
);
@@ -263,6 +272,6 @@ export class DashboardComponent implements OnInit {
try {
document.cookie = `lang=${language}; expires=Thu, 18 Dec 2050 12:00:00 UTC; path=/`;
} catch (e) { }
this.document.location.href = `${language === 'en' ? '' : '/' + language}/${this.stateService.network}`;
this.document.location.href = `/${language}/${this.stateService.network}`;
}
}

View File

@@ -11,6 +11,24 @@ export interface Transaction {
// Custom properties
firstSeen?: number;
feePerVsize?: number;
effectiveFeePerVsize?: number;
ancestors?: Ancestor[];
bestDescendant?: BestDescendant | null;
cpfpChecked?: boolean;
deleteAfter?: number;
}
interface Ancestor {
txid: string;
weight: number;
fee: number;
}
interface BestDescendant {
txid: string;
weight: number;
fee: number;
}
export interface Recent {
@@ -98,6 +116,7 @@ export interface Block {
}
export interface Address {
electrum?: boolean;
address: string;
chain_stats: ChainStats;
mempool_stats: MempoolStats;

View File

@@ -8,3 +8,20 @@ export interface OptimizedMempoolStats {
mempool_byte_weight: number;
vsizes: number[] | string[];
}
interface Ancestor {
txid: string;
weight: number;
fee: number;
}
interface BestDescendant {
txid: string;
weight: number;
fee: number;
}
export interface CpfpInfo {
ancestors: Ancestor[];
bestDescendant: BestDescendant | null;
}

View File

@@ -15,13 +15,12 @@ export interface WebsocketResponse {
tx?: Transaction;
rbfTransaction?: Transaction;
transactions?: TransactionStripped[];
donationConfirmed?: boolean;
loadingIndicators?: ILoadingIndicators;
backendInfo?: IBackendInfo;
'track-tx'?: string;
'track-address'?: string;
'track-asset'?: string;
'watch-mempool'?: boolean;
'track-donation'?: string;
}
export interface MempoolBlock {
@@ -35,14 +34,24 @@ export interface MempoolBlock {
}
export interface MempoolInfo {
size: number;
bytes: number;
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 TransactionStripped {
txid: string;
fee: number;
weight: number;
vsize: number;
value: number;
}
export interface IBackendInfo {
hostname: string;
gitCommit: string;
version: string;
}

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { CpfpInfo, OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { Observable } from 'rxjs';
import { StateService } from './state.service';
import { WebsocketResponse } from '../interfaces/websocket.interface';
@@ -77,7 +77,19 @@ export class ApiService {
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/donations');
}
getContributor$(): Observable<any[]> {
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/contributors');
}
checkDonation$(orderId: string): Observable<any[]> {
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/donations/check?order_id=' + orderId);
}
getInitData$(): Observable<WebsocketResponse> {
return this.httpClient.get<WebsocketResponse>(this.apiBaseUrl + this.apiBasePath + '/api/v1/init-data');
}
getCpfpinfo$(txid: string): Observable<CpfpInfo> {
return this.httpClient.get<CpfpInfo>(this.apiBaseUrl + this.apiBasePath + '/api/v1/cpfp/' + txid);
}
}

View File

@@ -20,6 +20,7 @@ export class AudioService {
this.isPlaying = true;
this.audio.src = '../../../resources/sounds/' + name + '.mp3';
this.audio.load();
this.audio.volume = 0.65; // 65% volume
this.audio.play().catch((e) => {
console.log('Play sound failed' + e);
});

View File

@@ -85,6 +85,9 @@ export class ElectrsApiService {
}
getAddressesByPrefix$(prefix: string): Observable<string[]> {
if (prefix.toLowerCase().indexOf('bc1') === 0) {
prefix = prefix.toLowerCase();
}
return this.httpClient.get<string[]>(this.apiBaseUrl + this.apiBasePath + '/api/address-prefix/' + prefix);
}
}

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