Compare commits

..

1463 Commits

Author SHA1 Message Date
wiz
678977a2a0 Merge pull request #2585 from mempool/simon/search-bar-placeholder-update
Updating search bar placeholder
2022-09-28 19:15:54 +09:00
softsimon
f7548a6154 Updating search bar placeholder 2022-09-28 14:14:08 +04:00
wiz
b1cb3f3798 Merge pull request #2582 from mempool/simon/sync-assets-path-argv
Require the resources path as input to sync assets
2022-09-27 19:33:47 +09:00
softsimon
611d86f3f7 Require the resources path as input to sync assets 2022-09-27 01:14:29 +04:00
wiz
e402be1dd2 Merge pull request #2385 from mempool/nymkappa/bugfix/incorrect-log
Log correct maxmind mysql updates - fix stats import processed files counter
2022-09-25 08:10:52 +09:00
wiz
cc79b2b2a2 Merge branch 'master' into nymkappa/bugfix/incorrect-log 2022-09-25 08:03:40 +09:00
wiz
34d5f97c79 Merge pull request #2578 from mempool/simon/revert-info-button-color-change
Revert info button color change
2022-09-25 08:03:12 +09:00
wiz
16cb3de211 Merge branch 'master' into simon/revert-info-button-color-change 2022-09-25 07:50:37 +09:00
wiz
788377d174 Merge pull request #2550 from hunicus/add-py-ws-eg
Add python example for websocket api docs
2022-09-25 07:50:30 +09:00
wiz
666a03baf9 Merge branch 'master' into add-py-ws-eg 2022-09-25 07:39:11 +09:00
wiz
6235dc97a3 Merge pull request #2551 from hunicus/js-template-formatting
Fix spacing on websocket api examples
2022-09-25 07:38:59 +09:00
wiz
fd8d61e742 Merge pull request #2519 from mempool/nymkappa/bugfix/show-hybrid-nodes-chart
Show tor+clearnet node series in chart
2022-09-25 07:37:24 +09:00
wiz
cce82c12f0 Merge branch 'master' into nymkappa/bugfix/show-hybrid-nodes-chart 2022-09-25 07:13:47 +09:00
wiz
b3a5a52432 Merge pull request #2574 from mempool/nymkappa/bugfix/node-tor-badge
Only show tor badge in node page if actually running on tor only
2022-09-25 07:13:03 +09:00
wiz
bf08498b72 Merge branch 'master' into nymkappa/bugfix/node-tor-badge 2022-09-25 06:57:49 +09:00
wiz
ea461ad592 Merge pull request #2575 from mononaut/tx-flow-diagram-algo
Improve transaction flow diagram drawing algorithm
2022-09-25 06:57:25 +09:00
softsimon
96870bf934 Revert info button color change 2022-09-25 01:57:16 +04:00
wiz
ea52e4df35 Merge branch 'master' into tx-flow-diagram-algo 2022-09-25 05:26:41 +09:00
wiz
c4760578a4 Merge pull request #2569 from mempool/simon/clipboard-button-fix
Clipboard buttons fix
2022-09-25 05:25:30 +09:00
softsimon
b5027cd646 Merge pull request #2577 from mempool/simon/new-button-color
New button light color
2022-09-24 02:39:43 +02:00
softsimon
c79c1d9958 New button light color 2022-09-24 02:39:27 +02:00
softsimon
b496f075a8 Merge pull request #2576 from mempool/simon/new-button-colors
Updating info button styling
2022-09-24 02:31:24 +02:00
softsimon
eb28fd90e5 Updating info button styling 2022-09-24 02:30:36 +02:00
Mononaut
409e5a335f Improve tx flow diagram drawing algorithm 2022-09-23 21:25:41 +00:00
wiz
7c13f5d8de Merge branch 'master' into simon/clipboard-button-fix 2022-09-23 15:16:08 +09:00
wiz
29118dc0e8 Merge pull request #2568 from mempool/simon/svg-logo-bug-fix
Mempool svg logo bug fix
2022-09-23 15:16:00 +09:00
wiz
0dab6e4ab1 Merge branch 'master' into simon/svg-logo-bug-fix 2022-09-23 14:36:08 +09:00
nymkappa
6df731af58 Only show tor badge in node page if actually running on tor only 2022-09-22 18:35:16 +02:00
softsimon
63417c9179 Merge pull request #2573 from mempool/nymkappa/bugfix/ranking-page-tor
Fix "undefined" location in node ranking
2022-09-22 18:17:01 +02:00
softsimon
ab2adc48a3 Merge pull request #2510 from mempool/nymkappa/feature/zero-base-fee-tag
Show zero base fee tag on channels
2022-09-22 18:10:41 +02:00
softsimon
2c370ffccd Merge branch 'master' into nymkappa/feature/zero-base-fee-tag 2022-09-22 17:55:41 +02:00
nymkappa
575a79145e Show "Non-zero base fee" and fix base fee rounding issue 2022-09-22 16:39:57 +02:00
nymkappa
b7b1dfdeb5 Change naming in networks line chart + Fix y axis scaling 2022-09-22 16:09:26 +02:00
nymkappa
0f218ced47 Fix legend 2022-09-22 15:23:51 +02:00
nymkappa
1ead34d42d Show tor+clearnet node series in chart 2022-09-22 15:23:50 +02:00
nymkappa
387cebeb50 Fix "undefined" location in node ranking 2022-09-22 15:12:28 +02:00
wiz
2e0afefe63 Merge pull request #2572 from mempool/simon/adding-shared-app-module
Adding MempoolSharedModule
2022-09-22 07:52:14 +09:00
wiz
65c3a40039 Merge branch 'master' into simon/adding-shared-app-module 2022-09-22 07:42:08 +09:00
wiz
c34cb939b7 Merge pull request #2571 from mempool/simon/relative-path-imports
Use relative import paths
2022-09-22 07:41:52 +09:00
softsimon
e3abd3d5ef Adding MempoolSharedModule 2022-09-21 18:27:05 +02:00
softsimon
fa11cb0619 Use relative import paths 2022-09-21 17:23:45 +02:00
softsimon
7cbc87d3df Clipboard buttons fix
fixes #2566
2022-09-19 18:21:31 +02:00
softsimon
bc0af68d97 Mempool svg logo bug fix
fixes #2505
2022-09-19 13:27:30 +02:00
wiz
72bed3b062 Merge pull request #2548 from mempool/simon/block-search-result
Suggest block height in search result
2022-09-19 06:24:42 +09:00
wiz
59931afd62 Merge pull request #2558 from mempool/simon/mempool-node-group-page
Mempool node group page
2022-09-19 06:14:22 +09:00
softsimon
ad30ba9602 Updated mempool group page 2022-09-18 23:05:11 +02:00
wiz
1b2e7090c3 Merge branch 'master' into simon/mempool-node-group-page 2022-09-19 05:39:43 +09:00
wiz
05e8811fe9 Merge pull request #2563 from mononaut/mining-pool-preview
Add mining pool preview
2022-09-19 05:39:36 +09:00
wiz
04ed24feae Merge branch 'master' into mining-pool-preview 2022-09-19 05:29:29 +09:00
wiz
5c8d28bf1d Merge pull request #2562 from mononaut/tx-page-diagram
Tx page diagram
2022-09-19 05:29:18 +09:00
wiz
78cc33ab01 Merge branch 'master' into tx-page-diagram 2022-09-19 05:11:31 +09:00
wiz
da2260a62e Merge pull request #2561 from mononaut/optimize-tx-diagram
Optimize transaction diagram component
2022-09-19 05:11:24 +09:00
wiz
287756ea19 Merge branch 'master' into optimize-tx-diagram 2022-09-19 04:59:01 +09:00
wiz
691f9aade1 Merge pull request #2565 from mempool/simon/address-label-overflow-fix
Allow address label to overflow without pushing UI
2022-09-19 04:04:16 +09:00
softsimon
b255f68a83 Allow address label to overflow without pushing UI
fixes #2544
2022-09-18 20:50:07 +02:00
Mononaut
f7cd401e7a Add mining pool preview 2022-09-17 22:27:37 +00:00
Mononaut
0ca33f7b5b Handle special input/output types in tx diagram 2022-09-17 17:23:44 +00:00
softsimon
a43f0454f9 Mempool node group page 2022-09-17 11:06:49 +02:00
Mononaut
64f3a597a2 Add interactivity to tx sankey diagram 2022-09-17 01:20:08 +00:00
Mononaut
1e5cef4a62 Add sankey diagram to main tx page 2022-09-16 20:50:12 +00:00
Mononaut
5e1ca44a7f limit number of lines in tx svg diagram 2022-09-16 20:49:31 +00:00
wiz
0694e71b14 Merge pull request #2554 from mononaut/unfurler-logging
Add logging & syslog support to unfurler
2022-09-16 10:38:08 +09:00
Mononaut
b1aa7965d7 Handle unfurler puppeteer page init exception 2022-09-16 01:09:59 +00:00
Mononaut
25cc038dd3 Fix bisq unfurler crash loop 2022-09-16 00:49:07 +00:00
Mononaut
65b677238c Add logging & syslog support to unfurler 2022-09-15 18:35:37 +00:00
hunicus
7014ac2335 Fix spacing on ws esmodule api example 2022-09-13 07:38:17 -04:00
hunicus
19a86cbd59 Fix spacing on ws commonjs api example 2022-09-13 07:14:03 -04:00
hunicus
fc57effd5c Add python example for websocket api docs 2022-09-13 06:44:34 -04:00
softsimon
b6296fcbeb Suggest block height in search result 2022-09-12 19:20:22 +02:00
wiz
0a645431ae Merge pull request #2509 from mempool/nymkappa/bugfix/location-hover
Show tooltip on location is truncated
2022-09-12 05:17:10 +09:00
wiz
2d0b4f868e Merge pull request #2486 from WesVleuten/master
Add docker lightning backend config
2022-09-12 05:12:04 +09:00
wiz
23efacad70 Merge pull request #2543 from mononaut/block-preview-title-layout
Adjust block preview layout
2022-09-12 05:08:32 +09:00
wiz
deae7b28e6 Merge branch 'master' into block-preview-title-layout 2022-09-12 04:56:25 +09:00
wiz
dd5d85cc7a Merge pull request #2541 from mononaut/refactor-preview-routing
Refactor preview routes into separate module
2022-09-12 04:56:12 +09:00
wiz
da8044c073 Merge branch 'master' into refactor-preview-routing 2022-09-12 04:18:14 +09:00
wiz
33334fd94c Merge pull request #2542 from mononaut/isp-preview
Add Lightning ISP preview
2022-09-12 04:16:48 +09:00
wiz
047843b19a Merge branch 'master' into isp-preview 2022-09-12 03:09:30 +09:00
Mononaut
a74811cb7e improve preview block hash truncation 2022-09-11 01:17:46 +00:00
softsimon
ce530e24e2 Merge pull request #2540 from mempool/simon/dashboard-stats-skeleton-fix
Dashboard node statistics skeleton loader fix
2022-09-10 18:56:30 +02:00
Mononaut
0ac3352835 tweak block preview (height & hash side by side) 2022-09-10 16:11:08 +00:00
Mononaut
7d367572dc Add lighting ISP preview 2022-09-10 14:53:52 +00:00
Felipe Knorr Kuhn
e63096239e Merge branch 'master' into master 2022-09-09 23:39:25 -07:00
Felipe Knorr Kuhn
b53bd5149e Merge branch 'master' into nymkappa/bugfix/location-hover 2022-09-09 23:26:09 -07:00
Felipe Knorr Kuhn
1b08f94497 Merge branch 'master' into simon/dashboard-stats-skeleton-fix 2022-09-09 21:22:48 -07:00
Mononaut
9a5844bbdc Refactor preview routes into separate module 2022-09-10 01:00:45 +00:00
wiz
9a87b357fc Merge pull request #2539 from mempool/wiz/fix-unfurler-config-liquid
Fix unfurler config set network to liquid
2022-09-10 05:40:19 +09:00
softsimon
67675e1f79 Dashboard node statistics skeleton loader fix 2022-09-09 22:39:10 +02:00
wiz
1eda695630 Fix unfurler config set network to liquid 2022-09-10 05:39:03 +09:00
wiz
9591be6401 Merge pull request #2538 from mononaut/disable-spinners-in-previews
Disable block viz/map loading spinners on previews
2022-09-10 04:29:38 +09:00
wiz
4089e4e8d1 Merge branch 'master' into disable-spinners-in-previews 2022-09-10 04:06:11 +09:00
wiz
cbb8997d5c Merge pull request #2535 from mononaut/preview-header-tweaks
Add network to preview headers & inc font size
2022-09-10 04:06:00 +09:00
Mononaut
f8fbef78bf Disable block viz/map loading spinners on previews 2022-09-09 19:01:32 +00:00
Mononaut
4fb77a9a45 Add network to preview headers & inc font size 2022-09-09 18:15:36 +00:00
wiz
1a8102f91c Merge pull request #2534 from mempool/simon/removing-double-row
Removing extra capacity row
2022-09-09 19:51:32 +02:00
softsimon
a8188a3536 Removing extra capacity row 2022-09-09 19:42:44 +02:00
wiz
0e090f940a Merge pull request #2367 from mempool/simon/eslint-triple-equals
Tooling: Eslint force triple equals
2022-09-09 19:20:03 +02:00
wiz
dd9ba701ad Merge pull request #2506 from knorrium/knorrium/fix_docker_start_script
Fix sed command for the pools json urls
2022-09-09 19:18:55 +02:00
wiz
dbfb886475 Merge pull request #2516 from mempool/nymkappa/feature/show-tor-node-page
Show that we don't know where a node is because it's running on tor
2022-09-09 19:16:49 +02:00
wiz
1a2e336c18 Change "Running on Tor" to "Exclusively on Tor" 2022-09-10 02:16:06 +09:00
wiz
e6bc15a9e1 Merge branch 'master' into nymkappa/feature/show-tor-node-page 2022-09-09 18:37:35 +02:00
wiz
6d75a2284e Merge pull request #2507 from mempool/nymkappa/feature/cltv
When using clightning, use listchannels.delay as cltv_delta
2022-09-09 18:27:28 +02:00
wiz
3a63375499 Merge branch 'master' into nymkappa/feature/cltv 2022-09-09 18:14:34 +02:00
wiz
d6d0c42691 Merge pull request #2521 from mononaut/fix-liquid-cb-previews
Fix preview tx diagram for zero value transactions
2022-09-09 18:13:59 +02:00
wiz
b30483572d Merge branch 'master' into nymkappa/feature/cltv 2022-09-09 18:06:44 +02:00
wiz
c92fcd20f7 Merge branch 'master' into fix-liquid-cb-previews 2022-09-09 18:04:40 +02:00
wiz
ffbb4e0b9e Merge pull request #2520 from mempool/nymkappa/bugfix/skeleton-label
Fix wrong skeleton labels
2022-09-09 18:03:37 +02:00
wiz
c98e95751f Merge branch 'master' into nymkappa/bugfix/skeleton-label 2022-09-09 17:33:14 +02:00
wiz
5e1f891f02 Merge pull request #2526 from mempool/nymkappa/bugfix/i18n
Lightning dashboard -> Lightning network
2022-09-09 17:33:10 +02:00
wiz
2aeccd72e9 Merge pull request #2527 from mempool/nymkappa/feature/isp-country-map-stats
ISP and Country node lists header
2022-09-09 17:32:56 +02:00
wiz
478d8ce70d Merge branch 'master' into nymkappa/feature/isp-country-map-stats 2022-09-09 17:04:24 +02:00
wiz
7087f7a78c Merge pull request #2524 from mempool/nymkappa/bugfix/isp-chart-color
Update ISP pie chart colors
2022-09-09 17:03:57 +02:00
wiz
91c607b0e8 Merge branch 'master' into nymkappa/bugfix/isp-chart-color 2022-09-09 16:19:43 +02:00
wiz
9c025e79ca Merge pull request #2532 from mempool/wiz/add-more-community-integrations-about-page 2022-09-09 16:02:53 +02:00
nymkappa
faec398cf0 Log correct maxmind mysql updates - fix stats import processed files counter 2022-09-09 14:59:49 +02:00
nymkappa
dcfcac2cc6 Show summary stats and world map in isp and country node list page 2022-09-09 14:56:18 +02:00
wiz
769ca5794a Merge branch 'master' into wiz/add-more-community-integrations-about-page 2022-09-09 14:53:52 +02:00
nymkappa
004768132b Show clearnet nodes on world map 2022-09-09 14:53:33 +02:00
wiz
12c188266a Merge pull request #2078 from erikarvstedt/shrink-frontend-size
frontend: Don't copy `resources`, shrink static size
2022-09-09 14:52:39 +02:00
Erik Arvstedt
22def9b01c frontend: Don't copy resources to language dirs
Since 355e89ce5, the frontend references resources via root-relative URLs.
This means that `resources` dirs in the language dirs are no longer
accessed and can be removed.

Achieve this by defining a specific `assets` production config that
doesn't include `src/resources`.

As of fd35c8f4a, this shrinks the frontend size by 55% (279M -> 124M).

Also, the nginx location configs now can be simplified.
2022-09-09 14:42:55 +02:00
wiz
2d2b7d3a9f Add more Community Integrations on About page 2022-09-09 19:25:25 +09:00
Felipe Knorr Kuhn
aa51484b0b Merge branch 'master' into master 2022-09-08 10:03:36 -07:00
nymkappa
aa1519c18e Show zero base fee tag on channels 2022-09-08 18:57:12 +02:00
softsimon
249a65bb57 Merge pull request #2518 from mempool/nymkappa/feature/only-scan-closed-chan-new-block
Only scan for closed channels when there is a new block
2022-09-08 17:44:10 +02:00
softsimon
b5f6fdecbf Merge pull request #2517 from mempool/nymkappa/bugfix/missing-loaders
Add skeleton loader in node per isp/country lists
2022-09-08 15:59:21 +02:00
softsimon
f015b165ee Merge pull request #2512 from mempool/nymkappa/feature/closed-channel-info
If a channel is closed, show closing date instead of last update
2022-09-08 15:49:39 +02:00
softsimon
4cb6418f83 Merge pull request #2504 from mempool/nymkappa/bugfix/placeholder-channel-page
Show '-' when value is not defined in channel page
2022-09-08 15:39:15 +02:00
nymkappa
5a0ffee58b Fix missing space between value and label 2022-09-08 14:33:46 +02:00
wiz
22091e05ac Merge pull request #2529 from mempool/nymkappa/special-feature-for-wiz-because-its-cool/change-isp-threshold
Change isp pie chart threshold from 0.5% to 0.4%
2022-09-07 22:16:27 +02:00
nymkappa
3a1da0eb4a Change isp pie chart threshold from 0.5% to 0.4% 2022-09-07 21:53:42 +02:00
nymkappa
1a2c0b7843 Lightning dashboard -> Lightning network 2022-09-07 15:38:48 +02:00
wiz
24c8ae2002 Merge pull request #2523 from mempool/nymkappa/bugfix/fix-ln-seo
Fix missing seo in lightning pages
2022-09-07 15:37:30 +02:00
nymkappa
51bf4f769f Fix missing seo in lightning pages 2022-09-07 15:25:03 +02:00
wiz
eaa5c0fb33 Merge pull request #2522 from mempool/nymkappa/feature/node-world-map
Show clearnet nodes on world map
2022-09-07 15:23:34 +02:00
wiz
d0b4d1da4a Merge branch 'master' into nymkappa/feature/node-world-map 2022-09-07 15:03:46 +02:00
wiz
07978bc3d4 Merge pull request #2454 from Emzy/ops/cahnge-restart
Remove the mempool restart script in prod install
2022-09-07 15:02:34 +02:00
wiz
11d6b372ba Merge pull request #2096 from erikarvstedt/backend-packaging
Simplify packaging for backend
2022-09-07 15:01:18 +02:00
wiz
04b4c61f83 Merge pull request #2500 from mempool/nymkappa/bugfix/node-list-location-label
Renamed "City" to "Location"
2022-09-07 15:00:23 +02:00
nymkappa
d9483dbd7a Update ISP pie chart colors 2022-09-07 10:10:25 +02:00
wiz
01588305fc Merge branch 'master' into nymkappa/bugfix/node-list-location-label 2022-09-06 22:58:59 +02:00
wiz
4fe3c308fe Merge pull request #2502 from mempool/nymkappa/bugfix/node-per-country
Show 0 sats when country has no liquidity
2022-09-06 22:58:27 +02:00
wiz
bdf60b2b68 Merge branch 'master' into nymkappa/bugfix/node-per-country 2022-09-06 22:47:07 +02:00
wiz
c57e9706cd Merge pull request #2501 from mempool/nymkappa/bugfix/capacity-liquidity
Renamed capacity to liquidity
2022-09-06 22:40:52 +02:00
nymkappa
367c06dca6 Show clearnet nodes on world map 2022-09-06 19:33:07 +02:00
softsimon
e3f6767259 Merge pull request #2472 from hunicus/lightning-api-docs
Add lightning api docs
2022-09-06 20:11:56 +03:00
Mononaut
b608b66823 Fix tx diagram for zero value transactions 2022-09-06 17:03:41 +00:00
softsimon
a1895a66b3 Merge pull request #2469 from mempool/nymkappa/bugfix/only-show-active-channels-in-map
Only show active channels on world map
2022-09-06 20:01:44 +03:00
nymkappa
2d52dcd867 Fix wrong skeleton labels 2022-09-06 16:43:22 +02:00
hunicus
5d986e86de Add signet lightning api docs 2022-09-06 10:29:04 -04:00
hunicus
25ee1acde9 Add testnet lightning api docs 2022-09-06 10:28:56 -04:00
hunicus
50b9644bd0 Add mainnet lightning api docs 2022-09-06 10:28:43 -04:00
softsimon
474b94f9af Merge pull request #2485 from mononaut/restyle-lightning-previews
Restyle lightning preview titles to match main pages
2022-09-06 15:03:52 +03:00
nymkappa
eb18625802 Only scan for closed channels when there is a new block 2022-09-06 11:42:19 +02:00
nymkappa
d536d63d69 Add skeleton loader in node per isp/country lists 2022-09-06 11:01:46 +02:00
nymkappa
efb18c7548 Show that we don't know where a node is because it's running on tor 2022-09-06 10:51:14 +02:00
Wes van der Vleuten
5eab47674c Fixed forgotten CLN instance 2022-09-05 19:35:55 +02:00
Wes van der Vleuten
50ae075b1f Fixed CLN to CLIGHTNING 2022-09-05 19:02:36 +02:00
nymkappa
5389928c49 Show closing date in closed channel list 2022-09-04 19:39:28 +02:00
nymkappa
5086f132f8 If a channel is closed, show closing date instead of last update 2022-09-04 19:12:01 +02:00
nymkappa
206edb7613 Show tooltip on location is truncated 2022-09-04 12:02:40 +02:00
nymkappa
a75262d79e Only show active channels on world map 2022-09-04 09:35:31 +02:00
nymkappa
dac3a43c1b When using clightning, use listchannels.delay as cltv_delta 2022-09-04 09:23:49 +02:00
Felipe Knorr Kuhn
a2dd0baaf6 Fix sed command for the pools json urls 2022-09-03 09:23:55 -07:00
nymkappa
3801f988ba Show '-' when value is not defined in channel page 2022-09-02 16:17:48 +02:00
Erik Arvstedt
34c8ad614a backend: Rename build variable DOCKER_COMMIT_HASH -> MEMPOOL_COMMIT_HASH
This var is useful for all build methods, not only Docker.

This is an internal renaming that doesn't change the public Docker
backend image API.
2022-09-02 12:50:44 +02:00
Erik Arvstedt
19bb8988f8 docker/init.sh: Remove unused code
This code has no effect because string `master` does not exist in `api/backend-info.ts`.
2022-09-02 12:50:44 +02:00
Erik Arvstedt
be72c5109a backend: Create npm script package
This script creates a directory `backend/package` which only contains the
files required by the backend at runtime:
- The contents of `dist`
- `node_modules` minus `typescript` and `@typescript-eslint`.
  These packages are build-only and are larger than the remaining whole package.

By using only `backend/package` in the Docker image,
the backend content size in the image is decreased by 70% to 31M.
This, along with the improved copying in the Dockerfile, reduces the
backend image size by 44% to 200M.
(Step `RUN chown -R 1000:1000 /backend ...` created a layer that effectively
duplicated the backend.)
2022-09-02 12:50:43 +02:00
Erik Arvstedt
d591f7c456 backend: Fetch package version at build time
Extract `fetch-version.ts` which is called at build time to create
file `dist/api/version.json`.
This file is read by `backend-info.ts` at runtime.

This also fixes handing over the Git commit hash to the backend app
in the Docker backend image, which was broken as of 2022-07-12.
(Reason: The commit hash was previously required at runtime, but was
only provided at build time.)
2022-09-02 12:50:43 +02:00
Erik Arvstedt
5683f639ed backend: Read mtgox-weekly.json from dist 2022-09-02 11:10:47 +02:00
Erik Arvstedt
8f0fc3af57 backend: Add config file env var 2022-09-02 11:08:42 +02:00
nymkappa
dcd55d9757 Fixes #2495 2022-09-02 10:28:54 +02:00
nymkappa
83df23a902 Renamed capacity to liquidity 2022-09-02 10:23:02 +02:00
nymkappa
ee23d1695d Use shared component in node ranking list 2022-09-02 10:08:25 +02:00
nymkappa
88a36f4378 Renamed "City" to "Location" 2022-09-02 09:30:07 +02:00
Felipe Knorr Kuhn
1aad4f2926 Merge branch 'master' into simon/eslint-triple-equals 2022-09-01 22:51:07 -07:00
Mononaut
2a28ccc758 Update block, address & tx preview layouts 2022-09-01 17:01:31 +00:00
Mononaut
4ee5ef336c Move lightning preview headers to top bar 2022-09-01 15:49:15 +00:00
Mononaut
3da76892d5 Restyle ln preview titles to match main pages 2022-09-01 15:49:12 +00:00
wiz
9047cb5998 Merge pull request #2484 from mononaut/new-default-preview-img
Use new mempool preview image as default
2022-09-01 11:09:21 +02:00
wiz
869cff89c6 Merge pull request #2483 from mononaut/fix-unfurler-race-condition-again
Fix unfurler race condition again
2022-09-01 10:18:59 +02:00
Wes van der Vleuten
95cd01d1fa Accept the CLA for @WesVleuten 2022-09-01 07:51:18 +02:00
Wes van der Vleuten
ad753b9d16 Added missing backend docker config 2022-09-01 06:50:42 +02:00
Mononaut
c155598c08 Use new mempool preview image as default 2022-08-31 20:40:24 +00:00
Mononaut
80dfa0e937 Fix null timestamps on transaction previews 2022-08-31 18:21:24 +00:00
Mononaut
5922ff0f40 Better fix for unfurler race condition 2022-08-31 17:24:56 +00:00
wiz
10bca8f665 Merge pull request #2480 from mempool/simon/node-alias-ellipsis
Node aliase ellipsis
2022-08-31 15:35:17 +02:00
softsimon
0bc310243f Node aliase ellipsis
fixes #2455
2022-08-31 16:21:16 +03:00
wiz
8a2925ab0c Merge pull request #2474 from mempool/simon/search-results-greyed
Grey out inactive search results
2022-08-31 14:55:40 +02:00
wiz
3f750fb8d4 Merge pull request #2478 from mempool/nymkappa/bugfix/mysql-inject
Fixes possible mysql injectin in channels.api
2022-08-31 14:52:14 +02:00
wiz
38324575e8 Merge branch 'master' into simon/search-results-greyed 2022-08-31 14:45:04 +02:00
wiz
1a10acf8ce Merge branch 'master' into nymkappa/bugfix/mysql-inject 2022-08-31 14:41:56 +02:00
wiz
799e8d9e23 Merge pull request #2475 from mempool/simon/channel-tx-details-button
Channel txs details buttons
2022-08-31 14:41:25 +02:00
wiz
454166dbca Merge pull request #2477 from mempool/nymkappa/bugfix/node-header-layout
Fix node header layout
2022-08-31 14:38:11 +02:00
wiz
c9343f56d6 Merge pull request #2476 from mempool/nymkappa/feature/closed-channels-sort
Sort closed channels by closing_date, updated_at
2022-08-31 14:34:17 +02:00
wiz
b389457092 Merge branch 'master' into simon/channel-tx-details-button 2022-08-31 14:27:18 +02:00
wiz
d5f8ce00b7 Merge branch 'master' into nymkappa/bugfix/node-header-layout 2022-08-31 14:23:14 +02:00
wiz
78298d16d7 Merge branch 'master' into nymkappa/feature/closed-channels-sort 2022-08-31 14:22:25 +02:00
wiz
4f8c36df35 Merge pull request #2448 from mempool/simon/search-improvements
Typeahead loading spinner and regex fixes
2022-08-31 14:21:31 +02:00
wiz
e65d2a522f Merge pull request #2470 from mempool/nymkappa/bugfix/node-update
Consider channels updates as well for node updated at field
2022-08-31 14:16:05 +02:00
wiz
e357a75b70 Merge branch 'master' into simon/search-improvements 2022-08-31 13:56:06 +02:00
nymkappa
ff1aae853e Save latest node channel update in node.updated_at field in db 2022-08-31 09:37:19 +02:00
nymkappa
08833b08a0 Fix possible mysql injectin in channels.api 2022-08-31 08:53:21 +02:00
nymkappa
e8151e8393 Fix node header layout 2022-08-31 08:17:42 +02:00
nymkappa
434963e8a0 Sort closed channels by closing_date, updated_at 2022-08-31 07:44:18 +02:00
softsimon
48a7f8a3ee Channel txs details buttons 2022-08-31 01:52:32 +02:00
wiz
9131521e7d Merge pull request #2473 from mononaut/hotfix-unfurler
hotfix to rollback broken unfurler
2022-08-31 00:01:21 +02:00
softsimon
c593ded864 Grey out inactive search results 2022-08-30 23:58:58 +02:00
Mononaut
9dc45d9db3 hotfix to rollback broken unfurler 2022-08-30 21:53:52 +00:00
wiz
8f060d3d65 Merge pull request #2466 from mononaut/unfurler-config-semantics
Change unfurler puppeteer config toggle to ENABLED
2022-08-30 23:31:30 +02:00
wiz
4d7ae95d4f Merge pull request #2467 from mononaut/fix-unfurler-race-condition
Fix unfurler navigation race condition
2022-08-30 23:29:39 +02:00
softsimon
a2e6b265d3 Search bar fixes. 2022-08-30 22:36:13 +02:00
wiz
ffc9081e1a Merge pull request #2435 from mempool/simon/svg-logos
Replacing all PNGs with inline SVG
2022-08-30 22:29:38 +02:00
wiz
a0c54531c0 Merge branch 'master' into simon/svg-logos 2022-08-30 22:07:02 +02:00
softsimon
0dfda66578 Typeahead loading spinner and regex fixes 2022-08-30 21:29:44 +02:00
wiz
70eb0abb7e Merge branch 'master' into fix-unfurler-race-condition 2022-08-30 21:09:58 +02:00
wiz
91355c0936 Merge pull request #2468 from mempool/nymkappa/bugfix/ipv6-cln
CLN - Add brakets around ipv6
2022-08-30 21:09:18 +02:00
wiz
b690dcaabc Merge branch 'master' into nymkappa/bugfix/ipv6-cln 2022-08-30 20:58:06 +02:00
wiz
a5e532d485 Merge pull request #2433 from mempool/simon/lightning-node-channel-skeleton-loaders
Node and Channel page skeleton loaders
2022-08-30 20:57:50 +02:00
nymkappa
c150129d74 CLN - Add brakets around ipv6 2022-08-30 20:31:04 +02:00
Mononaut
313d8d6a53 Fix unfurler navigation race condition 2022-08-30 18:21:18 +00:00
Mononaut
5a339c382f Change unfurler puppeteer config toggle to ENABLED 2022-08-30 18:20:52 +00:00
wiz
ef16f3bd68 Merge branch 'master' into simon/lightning-node-channel-skeleton-loaders 2022-08-30 20:10:10 +02:00
wiz
c289f821e4 Merge pull request #2460 from mempool/nymkappa/bugfix/asn-mapping
Harcode lunanode, FDCservers and cogent asn
2022-08-30 20:09:31 +02:00
wiz
51d35ec7d2 Merge branch 'master' into nymkappa/bugfix/asn-mapping 2022-08-30 19:45:31 +02:00
wiz
b36a7a2bcf Merge pull request #2403 from mempool/simon/da-api-handle-error
Fix for difficulty adjustment throwing error before in sync
2022-08-30 19:43:10 +02:00
wiz
d9320574d8 Merge branch 'master' into simon/da-api-handle-error 2022-08-30 19:35:57 +02:00
wiz
abb2ce5146 Merge pull request #2462 from mempool/nymkappa/bugfix/cant-click
Fix: `Can't click on channel in the box view` #2459
2022-08-30 18:21:48 +02:00
wiz
64a1ba3ac3 Merge branch 'master' into nymkappa/bugfix/cant-click 2022-08-30 17:50:54 +02:00
nymkappa
b6bebad14d Fix: Can't click on channel in the box view #2459 2022-08-30 17:33:13 +02:00
wiz
b30c6a6147 Merge pull request #2461 from pedromvpg/master
Replace logo with stacked version for open graph images
2022-08-30 17:26:52 +02:00
Pedro
a62dfe55cf Replace logo with stacked version for open graph images 2022-08-30 16:15:35 +01:00
nymkappa
cd2ef20d5c Harcode lunanode, FDCservers and cogent asn 2022-08-30 17:06:48 +02:00
wiz
7f2e68dae4 Merge pull request #2458 from mempool/wiz/add-missing-unfurler-nginx-route
[ops] Add missing unfurl nginx route
2022-08-30 16:55:26 +02:00
wiz
dc8256489b Merge pull request #2457 from pedromvpg/master
Open graph image updates
2022-08-30 16:51:05 +02:00
wiz
4a2c35c81b [ops] Add missing unfurl nginx route 2022-08-30 16:50:31 +02:00
Pedro
8094965a2c Update open graph images 2022-08-30 15:45:33 +01:00
Pedro
dfb35315e0 Merge branch 'master' of https://github.com/pedromvpg/mempool 2022-08-30 15:44:06 +01:00
Pedro
0aa5dff450 Update open graph images 2022-08-30 15:40:36 +01:00
Pedro
d1d4d0e5c4 Update open graph images 2022-08-30 15:38:10 +01:00
wiz
868136bb38 Merge pull request #2443 from mempool/nymkappa/feature/channel-map-color
Update channel map color
2022-08-30 15:54:07 +02:00
wiz
9bc1393981 Merge branch 'master' into nymkappa/feature/channel-map-color 2022-08-30 15:53:32 +02:00
wiz
af805f15c7 Merge pull request #2441 from mempool/nymkappa/feature/ip-check
Fix wrong ASN for Lunanode ip ranges
2022-08-30 15:53:19 +02:00
Stephan Oeste
f489ec6cee Remove the mempool restart script in prod install 2022-08-30 15:40:13 +02:00
wiz
56ec8b900c Merge branch 'master' into nymkappa/feature/channel-map-color 2022-08-30 15:35:18 +02:00
wiz
2f42dc9898 Merge pull request #2451 from Emzy/ops/unfurler-gpu
Install nvidia-driver, xorg and chromium if GPU is pressent on prod install
2022-08-30 15:35:01 +02:00
wiz
b9d56e8882 Merge pull request #2453 from hunicus/isp-typo-fix
Improve title for ISP map
2022-08-30 15:31:59 +02:00
Stephan Oeste
b0492f52a4 Install nvidia-driver, xorg and chromium if GPU is pressent on prod install 2022-08-30 15:27:47 +02:00
hunicus
a98c7a4b32 Improve title for ISP map 2022-08-30 09:10:15 -04:00
wiz
daeac2f894 Merge pull request #2425 from mononaut/unfurler-refactor
Unfurler fallback images & bisq support
2022-08-30 14:42:18 +02:00
wiz
0f5e4d3a15 Use config.SERVER.HOST instead of this.mempoolHost for fallbackImg 2022-08-30 14:30:32 +02:00
wiz
25c5ca731d Merge branch 'master' into unfurler-refactor 2022-08-30 13:49:02 +02:00
softsimon
8435c775ec Fixed dashboard skeleton loader text 2022-08-30 13:44:59 +02:00
softsimon
89f93d23b6 Merge pull request #2445 from mempool/nymkappa/bugfix/dashboard-percentages
When there is no stats for the past 7 days, don't show % changes
2022-08-30 13:39:43 +02:00
wiz
f97d3f57af Merge branch 'master' into unfurler-refactor 2022-08-30 13:22:38 +02:00
wiz
8e236e6594 Merge pull request #2450 from Emzy/ops/nginx-freebsd
No nginx configuration for FreeBSD
2022-08-30 13:22:18 +02:00
wiz
e1c98ceaa2 Merge pull request #2442 from mononaut/unfurler-optional-puppeteer
Option to disable puppeteer in unfurler
2022-08-30 13:21:12 +02:00
wiz
10e49fd77f Merge pull request #2449 from mempool/wiz/update-meta-description
Explore the full Bitcoin ecosystem with mempool.space
2022-08-30 13:17:30 +02:00
Stephan Oeste
e27b97f0f4 No nginx configuration for FreeBSD 2022-08-30 13:10:22 +02:00
wiz
32ee0ae908 Explore the full Bitcoin ecosystem with mempool.space 2022-08-30 12:57:21 +02:00
softsimon
3573912d8b Replacing Liquid Network and Bisq Markets with SVG. 2022-08-30 12:52:36 +02:00
wiz
2b333d513c Merge pull request #2438 from Emzy/ops/clone-lightning 2022-08-30 12:17:59 +02:00
wiz
bd690951e7 Merge pull request #2446 from Emzy/ops/cln-dirs 2022-08-30 12:13:45 +02:00
wiz
034b7fb516 Merge branch 'master' into unfurler-optional-puppeteer 2022-08-30 11:57:00 +02:00
wiz
b0fb93f7be Merge pull request #2447 from mempool/ops/setup-3-unfurlers
Add unfurler configs for 3 sites
2022-08-30 11:47:58 +02:00
wiz
0b4f17c129 Add unfurler configs for 3 sites 2022-08-30 11:28:25 +02:00
wiz
916042faab Merge pull request #2444 from mempool/nymkappa/bugfix/infinite-loading-no-channel
Fix infinite loading spinner in channel map when no active channel exists
2022-08-30 11:08:11 +02:00
softsimon
dd9ff41fde Skeleton loader updates 2022-08-30 10:54:05 +02:00
softsimon
0c71d505f2 Node and Channel page skeleton loaders 2022-08-30 10:53:11 +02:00
softsimon
948375f0e9 Merge pull request #2436 from mempool/nymkappa/bugfix/align-button
Align charts timestamp selector with charts menus
2022-08-30 10:51:04 +02:00
softsimon
b8f73b9495 Merge pull request #2439 from mempool/nymkappa/bugfix/loading-spinner-open-close-channels
Add loading animation for channel list
2022-08-30 10:47:04 +02:00
wiz
2debea28c8 Merge branch 'master' into unfurler-optional-puppeteer 2022-08-30 10:35:35 +02:00
wiz
f4f60a6b2e Merge branch 'master' into nymkappa/bugfix/infinite-loading-no-channel 2022-08-30 10:24:21 +02:00
wiz
c93334a849 Merge pull request #2423 from mempool/nymkappa/bugfix/historical-node-decoding
Save proper node socket format into db
2022-08-30 10:24:05 +02:00
Stephan Oeste
31c21137b0 Add mempool user to cln group and set data dirs rights on prod install 2022-08-30 10:22:05 +02:00
nymkappa
aec6d57fc3 When there is no stats for the past 7 days, don't show % changes 2022-08-30 09:08:34 +02:00
nymkappa
36663f0aa6 Fix infinite loading spinner in channel map when no active channel exists 2022-08-30 08:18:14 +02:00
nymkappa
6b248fb46d Change lightning channel map world color 2022-08-30 08:09:14 +02:00
Mononaut
c4e656e275 Option to disable puppeteer in unfurler 2022-08-29 23:30:10 +00:00
nymkappa
1d571c284c Fix wrong ASN for Lunanode ip ranges 2022-08-29 23:36:18 +02:00
wiz
1a5ee32565 Merge branch 'master' into nymkappa/bugfix/historical-node-decoding 2022-08-29 23:05:30 +02:00
wiz
feff3c52ef Merge pull request #2440 from Emzy/ops/change-crontab
Remove cache warmer from crontab. Now part of ./start script.
2022-08-29 22:33:20 +02:00
nymkappa
90c0ece93f Add loading animation for channel list 2022-08-29 22:25:43 +02:00
Stephan Oeste
45f2b016e1 Remove cache warmer from crontab. Now part of ./start script. 2022-08-29 22:25:35 +02:00
softsimon
f2377a5f92 Merge pull request #2437 from mempool/nymkappa/bugfix/channel-status-align
Fix open/close channel list button alignment
2022-08-29 22:16:04 +02:00
softsimon
d1c7105a84 Merge pull request #2430 from mempool/nymkappa/bugfix/mobile-addr-monospace
Use monospace font for addresses on mobile
2022-08-29 22:15:47 +02:00
Stephan Oeste
d36ae1476b Add mempool lighting explorer to the prod installer 2022-08-29 22:03:12 +02:00
softsimon
19d78ca519 Reducing size between table and header/toggle 2022-08-29 21:51:42 +02:00
nymkappa
676e83a872 Fix open/close channel list button alignment 2022-08-29 19:26:32 +02:00
nymkappa
70fd94dc1f Align charts timestamp selector with charts menus 2022-08-29 19:14:06 +02:00
softsimon
70d8a664e5 Replacing all PNGs with inline SVG 2022-08-29 19:13:47 +02:00
wiz
dcfaa9b474 Merge pull request #2432 from mempool/ops/always-return-true-stop-script
[ops] Set kill script to always return true
2022-08-29 12:45:31 +02:00
wiz
9c606a6240 [ops] Set kill script to always return true 2022-08-29 12:45:02 +02:00
wiz
586473a4e8 Merge pull request #2431 from mempool/nymkappa/feature/ln-chart-no-smooth 2022-08-29 11:59:27 +02:00
nymkappa
0ad8dc8529 Disable smooth flag for LN lines charts 2022-08-29 11:34:01 +02:00
nymkappa
680c85eee7 Use monospace font for addresses on mobile 2022-08-29 11:29:59 +02:00
wiz
c3604b5495 Merge pull request #2428 from mempool/ops/use-mainnet-cache-warmer
Use mainnet repo for nginx cache warmer script
2022-08-29 10:58:34 +02:00
wiz
73bf52dc58 Use mainnet repo for nginx cache warmer script 2022-08-29 10:58:07 +02:00
wiz
c8cde11ff1 Merge pull request #2427 from mempool/nymkappa/feature/update-warm-cache
Add new lightning endpoints to warm cache
2022-08-29 10:57:08 +02:00
wiz
e89d466827 Merge branch 'master' into nymkappa/feature/update-warm-cache 2022-08-29 10:42:52 +02:00
wiz
71a6b85b04 Merge pull request #2420 from mempool/simon/custom-lazy-loading-strategy
Custom lazy loading strategy
2022-08-29 10:42:37 +02:00
nymkappa
80d6ec580d Add new lightning endpoints to warm cache 2022-08-29 09:46:23 +02:00
nymkappa
0847e15d07 Update node sockets during historical import so we don't have to truncate 2022-08-29 09:01:07 +02:00
nymkappa
f25ec08f5e Format historical node sockets with update topology outputs 2022-08-29 08:43:26 +02:00
Mononaut
a5db46240e refactor unfurler routing & add fallback imgs 2022-08-29 01:23:20 +00:00
softsimon
b7662347c9 Trying 1500ms pre-load delay 2022-08-28 20:52:48 +02:00
softsimon
448d073bf2 Trying 800ms pre-load delay 2022-08-28 20:28:30 +02:00
softsimon
59c11379b2 Pre-load Docs 2022-08-28 20:06:15 +02:00
softsimon
2979f286cf Merge pull request #2424 from mempool/simon/transactions-list-fix-attempt
Possible fix for failing test
2022-08-28 18:26:28 +02:00
softsimon
1ca1d0b109 Possible fix for failing test 2022-08-28 17:48:51 +02:00
wiz
7e1ab55c01 Merge branch 'master' into simon/custom-lazy-loading-strategy 2022-08-28 17:25:26 +02:00
wiz
5878def72c Merge pull request #2409 from mempool/simon/channel-closing-type-header
Add closing type badge to channel header
2022-08-28 17:25:17 +02:00
wiz
0992258222 Merge branch 'master' into simon/channel-closing-type-header 2022-08-28 17:06:53 +02:00
wiz
6d027dd7ce Merge pull request #2416 from mempool/nymkappa/feature/ln-logger-tag
Update <lightning> log tag to show the network if non mainnet
2022-08-28 17:06:46 +02:00
wiz
622636e35f Merge branch 'master' into simon/channel-closing-type-header 2022-08-28 17:06:03 +02:00
wiz
3b8dc89b53 Merge pull request #2415 from mempool/nymkappa/bugfix/disable-ln-import-testsignet
Disable LN historical import if not mainnet
2022-08-28 17:05:28 +02:00
softsimon
5896ba9cf5 Merge pull request #2421 from hunicus/clipboard-fix
Fix clipboard for node pubkey on channel page
2022-08-28 16:49:55 +02:00
wiz
01e745e389 Merge branch 'master' into nymkappa/feature/ln-logger-tag 2022-08-28 16:46:05 +02:00
wiz
9377df9646 Merge pull request #2417 from mempool/nymkappa/bugfix/node-page-name-length
Truncate node alias if it's too long
2022-08-28 16:45:59 +02:00
softsimon
530cae3cdb Only pre-load Lightning if it's enabled by config 2022-08-28 16:36:42 +02:00
wiz
22f6bf60c0 Merge branch 'master' into nymkappa/bugfix/node-page-name-length 2022-08-28 16:33:20 +02:00
wiz
49813c9629 Merge pull request #2418 from mempool/nymkappa/bugfix/node-page-js-error
Fix js crash in node page and add loading spinner to channel tree chart
2022-08-28 16:33:12 +02:00
wiz
add01f547b Merge pull request #2422 from mempool/wiz/fix-typo-in-build-script
[ops] Fix typo in build script
2022-08-28 16:26:32 +02:00
wiz
f50bba1d39 [ops] Fix typo in build script 2022-08-28 16:26:06 +02:00
wiz
8d3a1c2daa Merge branch 'master' into nymkappa/bugfix/node-page-js-error 2022-08-28 16:20:53 +02:00
wiz
28b9205e95 Merge branch 'master' into nymkappa/bugfix/disable-ln-import-testsignet 2022-08-28 16:17:28 +02:00
wiz
38fa8f58b7 Merge pull request #2406 from Emzy/ops/fix-upgrade-script
Only source mysql_credentials if present in prod install
2022-08-28 16:17:21 +02:00
wiz
0018c865bd Merge pull request #2360 from mononaut/alt-tx-unfurls
Alternative transaction unfurl design
2022-08-28 16:17:07 +02:00
hunicus
bf0244f9dc Fix clipboard for node pubkey on channel page 2022-08-28 10:05:44 -04:00
wiz
6ae05842d9 Merge branch 'master' into alt-tx-unfurls 2022-08-28 15:56:54 +02:00
wiz
9cd61944d9 Merge pull request #2413 from mononaut/locale-preview-network-regex
Handle locale & preview in network detection regex
2022-08-28 15:56:36 +02:00
wiz
e7aa862e43 Merge branch 'master' into locale-preview-network-regex 2022-08-28 14:32:12 +02:00
wiz
5d0547af48 Merge pull request #2407 from Emzy/ops/late-start-cln
Start cln after Bitcoind in prod installer
2022-08-28 14:31:54 +02:00
wiz
a83b2ddeea Merge pull request #2414 from mononaut/fix-ln-map-unfurls
Fix unfurls for lightning pages with no geodata
2022-08-28 14:31:12 +02:00
wiz
1cd0796428 Merge branch 'master' into nymkappa/bugfix/disable-ln-import-testsignet 2022-08-28 14:24:05 +02:00
wiz
a2e4da840e Merge branch 'master' into fix-ln-map-unfurls 2022-08-28 14:15:37 +02:00
wiz
b7bf4ba010 Merge pull request #2419 from mempool/wiz/add-unfurl-to-ops-scripts
[ops] Update prod scripts for unfurler and cache warmer
2022-08-28 14:01:36 +02:00
wiz
8ec61dd603 Update ops scripts for unfurler and cache warmer 2022-08-28 14:00:20 +02:00
softsimon
3f16b53159 Custom lazy loading strategy 2022-08-28 13:43:57 +02:00
wiz
1eef5d40a5 Merge pull request #2387 from mempool/nymkappa/feature/ignore-invalid-gossip
Hardcode some condition to invalidate imported topology files
2022-08-28 13:17:43 +02:00
wiz
0b7aa8a83c Merge branch 'master' into alt-tx-unfurls 2022-08-28 12:45:41 +02:00
nymkappa
d8e87bccab Fix js crash in node page and add loading spinner to channel tree chart 2022-08-28 11:11:53 +02:00
nymkappa
930f1e4f09 Truncate node alias if it's too long 2022-08-28 08:58:41 +02:00
nymkappa
a65d54c549 Update <lightning> log tag to show the network if non mainnet 2022-08-28 08:37:25 +02:00
nymkappa
35de1d4d9a Disable LN historical import if not mainnet 2022-08-28 08:24:29 +02:00
nymkappa
6b049f2c33 Manually delete some lightning_stats rows once the topology import is completed 2022-08-28 07:59:23 +02:00
nymkappa
7ba8a3da84 Hardcode some condition to invalidate imported topology files 2022-08-28 07:59:23 +02:00
Mononaut
626b4e61cd Restyle transaction preview diagram 2022-08-27 21:23:23 +00:00
Mononaut
141789b034 handle locale & preview in network detection regex 2022-08-27 21:02:28 +00:00
softsimon
f19978345e Merge pull request #2412 from mempool/simon/missing-channel-inputs
Fixing missing channel openings
2022-08-27 22:33:27 +02:00
softsimon
be10cc65f4 Fixing missing channel openings 2022-08-27 22:32:56 +02:00
Mononaut
4ca87e730c Fix unfurls for lightning pages with no geodata 2022-08-27 19:02:22 +00:00
softsimon
d931ddc731 Merge pull request #2408 from mempool/simon/transactions-list-data-fetch-fix
Fixes multiple bugs with outspends and channels
2022-08-27 19:14:34 +02:00
wiz
54c3b2ba4a Merge pull request #2410 from mempool/wiz/add-prod-lightning-configs
[ops] Add production lightning backend configurations
2022-08-27 18:58:51 +02:00
wiz
6f87fd9c89 [ops] Add production lightning backend configurations 2022-08-27 18:57:59 +02:00
softsimon
88e6afadb2 Add closing type badge to channel header 2022-08-27 16:34:56 +02:00
softsimon
40dc476460 Fixes multiple bugs with outspends and channels
fixes #412
2022-08-27 16:01:08 +02:00
Stephan Oeste
c2c7448c45 Start cln after Bitcoind in prod installer 2022-08-27 15:58:01 +02:00
Stephan Oeste
3b2061bb5c Only source mysql_credentials if present in prod install 2022-08-27 15:52:42 +02:00
wiz
1a756c5fa9 Merge pull request #2405 from mempool/wiz/add-nginx-unfurl-entrypoints
[ops] Add nginx entrypoints for unfurler daemon
2022-08-27 14:17:56 +02:00
wiz
004dcebc19 [ops] Add nginx entrypoints for unfurler daemon 2022-08-27 14:17:17 +02:00
wiz
6d535441c0 Merge pull request #2404 from mempool/wiz/add-nginx-unfurl-placeholder
[ops] Add nginx placeholders for unfurlbot configuration
2022-08-27 13:57:19 +02:00
wiz
119de111e4 Merge pull request #2402 from mempool/simon/fix-broken-nodes-on-isp
Fix for broken SQL to load Lightning nodes on ISP page
2022-08-27 13:56:45 +02:00
wiz
bba9f2608a [ops] Add nginx placeholders for unfurlbot configuration 2022-08-27 13:55:30 +02:00
softsimon
936921781e Fix for difficulty adjustment throwing error before in sync 2022-08-27 10:25:24 +02:00
softsimon
d051538c6a Fix for broken SQL to load Lightning nodes on ISP page 2022-08-27 09:54:16 +02:00
wiz
69d4ba18d5 Merge pull request #2333 from mempool/fix/difficulty-api
Fix: Difficulty API (REST) with frontend fixes
2022-08-27 09:47:10 +02:00
wiz
738d1f8007 Merge branch 'master' into fix/difficulty-api 2022-08-27 09:35:53 +02:00
wiz
6092a7d9ed Merge pull request #2399 from mempool/simon/node-index-capacity-channels
Store capacity and channels in nodes table
2022-08-26 22:22:20 +02:00
softsimon
ba7b65a978 Fetch capacity and channels from nodes table 2022-08-26 16:35:24 +04:00
softsimon
85c428be25 Store capacity and channels in nodes table 2022-08-26 02:31:14 +04:00
wiz
beac979c32 Merge pull request #2397 from mempool/simon/rename-search-placeholder
Search bar placeholder text update
2022-08-26 00:37:35 +09:00
wiz
62be755fc4 Merge branch 'master' into simon/rename-search-placeholder 2022-08-26 00:30:35 +09:00
softsimon
cfb5f2f3b5 Search bar placeholder text update 2022-08-25 19:29:21 +04:00
wiz
a48f116bcd Merge pull request #2388 from Emzy/Fix-syslog-path
Fix path for newsyslog configs in prod installer
2022-08-26 00:28:56 +09:00
wiz
d045f2750b Merge pull request #2392 from mempool/simon/node-alias-search-numbers
Numbers not included in node alias search
2022-08-26 00:22:57 +09:00
wiz
686cfb6d2f Merge branch 'master' into simon/node-alias-search-numbers 2022-08-26 00:12:15 +09:00
wiz
dda8c7fb14 Merge pull request #2394 from mempool/simon/empty-tx-value-fix
Fix for empty tx value
2022-08-26 00:11:28 +09:00
wiz
5eac69b6df Merge branch 'master' into simon/empty-tx-value-fix 2022-08-25 23:53:29 +09:00
wiz
144eb558c4 Merge pull request #2381 from mempool/nymkappa/bugfix/useless-mysql-query-channel-tree
Do not fetch node stats for channel tree graph
2022-08-25 23:51:15 +09:00
wiz
a9e1ce42df Merge branch 'master' into nymkappa/bugfix/useless-mysql-query-channel-tree 2022-08-25 23:32:18 +09:00
wiz
5d3ad78611 Merge pull request #2377 from mempool/nymkappa/bugfix/missing-node-location
Node history chart full width if we can't display channel map
2022-08-25 23:32:06 +09:00
wiz
0add42c53b Merge branch 'master' into nymkappa/bugfix/missing-node-location 2022-08-25 23:09:34 +09:00
wiz
865b25d8df Merge pull request #2383 from mempool/nymkappa/feature/import-historical-nodes
Import historical nodes
2022-08-25 23:08:50 +09:00
softsimon
a5410178c8 Fix for empty tx value
fixes #2263
2022-08-25 17:24:42 +04:00
softsimon
b6bea12bca Numbers not included in node alias search 2022-08-25 15:14:54 +04:00
Mononaut
fafe40cef0 Alternative transaction unfurl design 2022-08-24 17:19:41 +00:00
wiz
1971d5d6b6 Merge pull request #2375 from mempool/nymkappa/bugfix/loading-spinner
Fix infitinite loading spinner and positioning
2022-08-25 01:18:31 +09:00
Stephan Oeste
dc4cd96fc0 Fix path for newsyslog configs in prod installer 2022-08-24 17:49:56 +02:00
wiz
cd57cfc861 Merge branch 'master' into nymkappa/bugfix/loading-spinner 2022-08-25 00:39:07 +09:00
wiz
ca6edb4bc4 Merge pull request #2373 from mempool/nymkappa/feature/title-update
Open/closed channel title updating based on selected mode
2022-08-25 00:38:46 +09:00
wiz
f51a4b4416 Merge branch 'master' into nymkappa/feature/title-update 2022-08-25 00:14:05 +09:00
wiz
f888011191 Merge pull request #2386 from mempool/wiz/dont-alert-for-warnings
[ops] Change keybase notifications to ERR or higher priority
2022-08-25 00:11:25 +09:00
wiz
d575f554ad Merge pull request #2389 from Emzy/small-fixes
Add .local/bin to cln's path and install git-lfs in prod install
2022-08-25 00:10:56 +09:00
wiz
b1b84c1e50 Merge pull request #2372 from mempool/nymkappa/bugfix/ln-network-history-chart
Hide subtitle if not widget
2022-08-24 23:22:44 +09:00
Stephan Oeste
fd8ed4bbdf Add .local/bin to cln's path and install git-lfs in prod install 2022-08-24 16:21:36 +02:00
wiz
c23aa67b0c Merge branch 'master' into nymkappa/bugfix/ln-network-history-chart 2022-08-24 22:56:03 +09:00
Jonathan Underwood
52ba1b7910 Merge branch 'master' into fix/difficulty-api 2022-08-24 22:48:37 +09:00
wiz
0afdaf116e Merge pull request #2370 from mononaut/fix-ln-preview-graphs
Fix lightning map heights on preview pages
2022-08-24 22:47:30 +09:00
Mononaut
8dd8c01aab Fix lightning graph heights on preview pages 2022-08-24 13:06:46 +00:00
wiz
71002ee119 [ops] Change keybase notifications to ERR or higher priority 2022-08-24 21:59:05 +09:00
wiz
5ad9572166 Merge pull request #2374 from mempool/nymkappa/bugfix/missing-closing-div
Add missing closing div
2022-08-24 20:13:18 +09:00
wiz
9fc753630a Merge branch 'master' into nymkappa/bugfix/missing-closing-div 2022-08-24 20:03:35 +09:00
wiz
00bd61e1d3 Merge branch 'master' into fix/difficulty-api 2022-08-24 20:01:52 +09:00
nymkappa
a151a90d2f Import historical nodes 2022-08-24 12:12:16 +02:00
nymkappa
4e9bc955e6 Fix channel map loading spinners 2022-08-24 10:04:48 +02:00
wiz
b121e46bcc Merge pull request #2362 from mempool/nymkappa/bugfix/db-deadlock
Await mysql queries in order to avoid deadlock
2022-08-24 15:59:33 +09:00
nymkappa
43cc9499b1 Check query input before running the mysql query 2022-08-24 08:35:02 +02:00
nymkappa
35512bef8d Do not fetch node stats for channel tree graph 2022-08-23 22:47:18 +02:00
nymkappa
755ac276f7 Node history chart full width if we can't display channel map 2022-08-23 18:00:40 +02:00
nymkappa
1fa882d59e Add missing closing div 2022-08-23 17:31:44 +02:00
wiz
23fab65216 Merge branch 'master' into nymkappa/bugfix/db-deadlock 2022-08-24 00:08:17 +09:00
nymkappa
4cceb008ea Open/closed channel title updating based on selected mode 2022-08-23 17:03:04 +02:00
wiz
7995058d86 Merge pull request #2371 from mempool/nymkappa/feature/node-page-layout
Update node page layout
2022-08-23 23:56:07 +09:00
wiz
e0c097e0dd Merge branch 'master' into nymkappa/feature/node-page-layout 2022-08-23 23:41:19 +09:00
wiz
9d9ead60a6 Merge pull request #2363 from mempool/nymkappa/bugfix/handle-esplora-error
Wrap esplora call into try/catch when scanning channel forensics
2022-08-23 23:40:31 +09:00
nymkappa
0a8b8cc75a Hide subtitle if not widget 2022-08-23 16:36:41 +02:00
nymkappa
c4914c2ced Update node page layout 2022-08-23 16:32:10 +02:00
wiz
a30bb4e6c0 Merge branch 'master' into nymkappa/bugfix/handle-esplora-error 2022-08-23 23:08:13 +09:00
wiz
7babc82f5d Merge pull request #2366 from mempool/nymkappa/feature/channel-tree-map
Create active channel tree map component
2022-08-23 23:06:56 +09:00
nymkappa
7d5ed66db1 Wrap esplora call into try/catch when scanning channel forensics 2022-08-23 15:53:15 +02:00
wiz
8c885e87d4 Merge branch 'master' into nymkappa/feature/channel-tree-map 2022-08-23 22:42:32 +09:00
wiz
4f009e0320 Merge branch 'master' into nymkappa/bugfix/db-deadlock 2022-08-23 22:36:07 +09:00
wiz
13c5e05044 Merge pull request #2361 from mempool/nymkappa/bugfix/cannot-update-channel-list
Fix "cannot update channel list" error
2022-08-23 22:14:49 +09:00
softsimon
2104570889 Tooling: Eslint force triple equals 2022-08-23 17:05:23 +04:00
wiz
f24d4124fb Merge branch 'master' into nymkappa/bugfix/cannot-update-channel-list 2022-08-23 21:32:14 +09:00
wiz
fc5525ec4a Merge pull request #2354 from mempool/nymkappa/feature/channe-map-trim
Improve channels world map performances
2022-08-23 21:31:57 +09:00
nymkappa
08b04c3264 Create active channel tree map component 2022-08-23 11:26:00 +02:00
nymkappa
b3735328b7 Await mysql queries in order to avoid deadlock 2022-08-22 22:31:21 +02:00
nymkappa
73d2930230 Fix "cannot update channel list" error 2022-08-22 22:15:15 +02:00
nymkappa
fd46ea82bf Fix channel map size 2022-08-22 22:06:31 +02:00
nymkappa
bd1d9573d6 Reduce api size for channel world map in ln dashboard - added spinner - update cache warmer 2022-08-22 21:36:02 +02:00
wiz
7fe9029a4e Merge pull request #2356 from mempool/nymkappa/feature/update-ranking-naming
Update node ranking naming convention and layout
2022-08-23 04:31:12 +09:00
wiz
0b3e8c3fef Merge branch 'master' into nymkappa/feature/update-ranking-naming 2022-08-23 04:13:35 +09:00
wiz
7ad1c15245 Merge pull request #2353 from mempool/nymkappa/feature/beta-tag-ln
Add "beta" tag next to lightning menu
2022-08-23 01:01:44 +09:00
wiz
29dd842404 Merge branch 'master' into nymkappa/feature/beta-tag-ln 2022-08-23 00:52:16 +09:00
wiz
6c4ad94b59 Merge pull request #2352 from mempool/nymkappa/feature/channel-node-page-title
Add "Lightning node" and "Lighning channel" page title
2022-08-23 00:52:06 +09:00
wiz
8fa65f170d Merge branch 'master' into nymkappa/feature/channel-node-page-title 2022-08-23 00:43:57 +09:00
wiz
9692ae5cdd Merge pull request #2351 from mempool/nymkappa/bugfix/useless-api-call
Remove useless api call in channel page
2022-08-23 00:43:46 +09:00
wiz
c780962892 Merge branch 'master' into nymkappa/bugfix/useless-api-call 2022-08-23 00:15:08 +09:00
wiz
f0283a3e17 Merge pull request #2343 from mempool/simon/channel-closing-reason
Fix for missing closing reason in channels list
2022-08-23 00:14:47 +09:00
nymkappa
0f6aec31fd Update node ranking naming convention
Matches ln dashboard layout with mining dashboard
2022-08-22 16:55:54 +02:00
wiz
6110d89bb4 Merge branch 'master' into simon/channel-closing-reason 2022-08-22 23:53:21 +09:00
wiz
713c0ef216 Merge pull request #2355 from mempool/simon/network-regex-matching-fix
Fix network regex matching
2022-08-22 23:53:00 +09:00
softsimon
63404edeec Fix for closing missing closing reason in channels list 2022-08-22 18:18:17 +04:00
softsimon
9af2d478c8 Fix network regex matching 2022-08-22 18:10:09 +04:00
nymkappa
0ea9b47604 Add "beta" tag next to lightning menu 2022-08-22 10:56:27 +02:00
nymkappa
2a26d532df Add "Lightning node" and "Lighning channel" page title 2022-08-22 09:17:23 +02:00
nymkappa
f7d475aa75 Remove useless api call in channel page 2022-08-22 09:07:09 +02:00
wiz
766dcddd36 Merge pull request #2345 from mempool/simon/node-alias-fulltext-search
Node alias fulltext search
2022-08-21 22:59:50 +09:00
wiz
4727b4bade Merge branch 'master' into simon/node-alias-fulltext-search 2022-08-21 22:29:18 +09:00
wiz
f48460687b Merge pull request #2338 from mempool/nymkappa/bugfix/missing-variable-ln
Add missing lightning configuration variables where needed
2022-08-21 22:28:56 +09:00
wiz
14bf256ab8 Merge branch 'master' into nymkappa/bugfix/missing-variable-ln 2022-08-21 22:18:09 +09:00
wiz
3201caf54e Merge pull request #2336 from mempool/nymkappa/feature/stop-updating-closed-channels
If a channel is closed, stop updating it
2022-08-21 22:17:08 +09:00
wiz
7c25fd61da Merge branch 'master' into nymkappa/feature/stop-updating-closed-channels 2022-08-21 22:05:52 +09:00
wiz
330ba7cb71 Merge pull request #2326 from mempool/nymkappa/feature/hide-map-if-no-geoloc
Hide map if there is not geolocation data available
2022-08-21 22:05:00 +09:00
wiz
3a02df28af Merge branch 'master' into nymkappa/feature/hide-map-if-no-geoloc 2022-08-21 21:48:34 +09:00
wiz
b66cfa23a8 Merge pull request #2325 from mempool/nymkappa/bugfix/isp-chart-ids
Fix missing isp ids on isp pie chart
2022-08-21 21:48:24 +09:00
wiz
9fd4adecb3 Merge branch 'master' into nymkappa/bugfix/isp-chart-ids 2022-08-21 21:12:31 +09:00
wiz
7c1e35ae3b Merge pull request #2324 from mempool/nymkappa/feature/improve-location
Create geolocation component to format geolocation data
2022-08-21 21:12:04 +09:00
wiz
8c9ac01123 Merge branch 'master' into nymkappa/feature/improve-location 2022-08-21 18:51:06 +09:00
wiz
c75ea29d26 Merge pull request #2323 from mempool/nymkappa/bugfix/node-size-map
Increase node size in channel map on node/channel pages
2022-08-21 18:50:43 +09:00
wiz
998dcc6f89 Merge branch 'master' into nymkappa/bugfix/node-size-map 2022-08-21 18:01:56 +09:00
wiz
daa5ebbcff Merge pull request #2322 from mempool/nymkappa/bugfix/indexing-message
Fix "indexing in progress" message in ln dashboard
2022-08-21 17:59:34 +09:00
wiz
454414179b Merge branch 'master' into nymkappa/bugfix/indexing-message 2022-08-21 17:13:02 +09:00
wiz
ad33890dea Merge pull request #2320 from mempool/nymkappa/bugfix/isp-chart-perc
Fix wrong tooltip % in isp pie chart
2022-08-21 17:11:44 +09:00
wiz
fe139651f5 Merge branch 'master' into nymkappa/bugfix/isp-chart-perc 2022-08-21 16:55:39 +09:00
softsimon
de3e89ac33 Node alias fulltext search
fixes #2238
2022-08-20 22:10:25 +04:00
wiz
13bac763a1 Merge pull request #2342 from mempool/simon/display-opening-closing-transactions 2022-08-20 21:05:14 +09:00
wiz
ffd8a527f2 Merge branch 'master' into simon/display-opening-closing-transactions 2022-08-20 20:40:46 +09:00
wiz
6b0d89e920 Merge pull request #2314 from mempool/nymkappa/bugfix/channel-list-pagination
Fix node channel list pagination
2022-08-20 20:39:00 +09:00
softsimon
13dca97505 Reduce default rows and optimize requests for lightning. 2022-08-20 15:37:56 +04:00
softsimon
fa83c2a26d Display opening and closing transactions on channel page
fixes #2340
2022-08-20 15:37:56 +04:00
wiz
ea931da38b Merge branch 'master' into nymkappa/bugfix/channel-list-pagination 2022-08-20 20:19:55 +09:00
wiz
406a65cfb6 Merge pull request #2335 from mempool/nymkappa/bugfix/most-connected-node-undefined
Fix undefined public_key
2022-08-20 20:18:30 +09:00
wiz
dd2c226354 Merge pull request #2311 from mempool/nymkappa/feature/closed-channel-import
Import historical channels
2022-08-20 19:09:16 +09:00
junderw
3a4982c5e6 Refactor Difficulty API logic 2022-08-20 13:02:22 +09:00
junderw
87c9f881c0 Refactor difficulty API logic 2022-08-20 11:24:48 +09:00
junderw
d700b5f145 Fix test setup 2022-08-20 09:53:02 +09:00
junderw
1bc2c18167 Fix tests 2022-08-20 09:53:02 +09:00
junderw
a00eb2736b Refactor Difficulty Adjustment calc + unit test it 2022-08-20 09:53:02 +09:00
junderw
bb1adf41e7 Update API docs for Difficulty API (REST) 2022-08-20 09:53:02 +09:00
junderw
772765959b Fix Difficulty API (REST) 2022-08-20 09:53:02 +09:00
nymkappa
2435d12181 Add missing lightning configuration variables where needed 2022-08-19 22:08:36 +02:00
nymkappa
5b9b717a93 Fix LN stats importer with new data "cleanupTopology" structure 2022-08-19 18:07:26 +02:00
wiz
64c5f1ce02 Merge branch 'master' into nymkappa/feature/closed-channel-import 2022-08-20 00:12:08 +09:00
wiz
b2d07d2d44 Merge pull request #2331 from mempool/nymkappa/bugfix/stats-timestamp
Use timestamp instead of date in stats tables
2022-08-20 00:11:46 +09:00
wiz
e46636f573 Merge branch 'master' into nymkappa/bugfix/stats-timestamp 2022-08-19 23:57:14 +09:00
wiz
4cf4efd3f2 Merge pull request #2319 from mempool/nymkappa/feature/import-json-topology
Import json topology
2022-08-19 23:57:03 +09:00
nymkappa
48dcf01199 If a channel is closed, stop updating it 2022-08-19 16:43:37 +02:00
nymkappa
64c07cf2d2 Fix undefined public_key 2022-08-19 16:38:16 +02:00
wiz
4e7b0b8650 Merge pull request #2334 from mempool/wiz/unpublish-github-sponsor
Unpublish the Github Sponsor links
2022-08-19 22:38:57 +09:00
wiz
59f0e2d345 Unpublish the Github Sponsor links 2022-08-19 22:38:18 +09:00
nymkappa
e7d99e9653 Insert channels from historical data 2022-08-19 12:34:38 +02:00
nymkappa
1ef4485a26 Use timestamp instead of date in stats table 2022-08-19 12:09:58 +02:00
wiz
c9a8b91c0b Merge branch 'master' into nymkappa/feature/import-json-topology 2022-08-19 18:54:03 +09:00
wiz
0dc950ab95 Merge pull request #2329 from mempool/nymkappa/bugfix/revert-2300
Reverted wrong fix in 2300
2022-08-19 17:46:49 +09:00
nymkappa
298edb6430 Reverted wrong fix in 2300 2022-08-19 08:55:45 +02:00
wiz
558ddec0a1 Merge pull request #2304 from mononaut/fix-sticky-block-error
Fix sticky error state on block page
2022-08-19 05:46:32 +09:00
wiz
9fca8de52f Merge pull request #2300 from slaninas/master
Fix difficulty adjustment
2022-08-19 05:46:14 +09:00
wiz
f4ee983807 Merge pull request #2328 from mempool/simon/remove-taproot-privacy-claims
Removing taproot privacy claims
2022-08-19 04:52:24 +09:00
softsimon
78f28c4bc3 Remove taproot privacy claims 2022-08-18 23:34:24 +04:00
wiz
cbd8936872 Merge pull request #2327 from mempool/simon/hide-features-before-activation
Hide features before feature activated
2022-08-19 04:32:59 +09:00
wiz
60fb61f70a Merge branch 'master' into simon/hide-features-before-activation 2022-08-19 04:12:22 +09:00
wiz
c767f39619 Merge pull request #2316 from mempool/nymkappa/feature/node-ranking-page
Create node rankings dashboard
2022-08-19 04:10:38 +09:00
wiz
daf3e269f4 Merge branch 'master' into nymkappa/feature/node-ranking-page 2022-08-19 03:51:55 +09:00
wiz
d2240532c1 Merge pull request #2315 from mempool/nykappa/feature/node-ranking
Refactor top nodes widgets, create related top 100 nodes pages
2022-08-19 03:51:31 +09:00
softsimon
e2f60a6761 Hide features before feature activated 2022-08-18 22:44:31 +04:00
nymkappa
eb1d6a4a78 Hide map if there is not geolocation data available 2022-08-18 18:29:11 +02:00
junderw
5ab05e4e12 Add contributors/junderw.txt 2022-08-19 01:14:54 +09:00
junderw
5d22023d19 Fix times
Co-authored-by: slaninas <slaninas@pm.me>
2022-08-19 01:14:54 +09:00
nymkappa
0e3e62fee8 Fix missing isp ids on isp pie chart 2022-08-18 17:15:05 +02:00
nymkappa
e437f2125d Create geolocation component to format geolocation data 2022-08-18 17:14:09 +02:00
nymkappa
64443d4b1b Increase node size in channel map on node/channel pages 2022-08-18 15:25:11 +02:00
nymkappa
42604cc6be Fix "indexing in progress" message in ln dashboard 2022-08-18 15:09:03 +02:00
nymkappa
9f60d787fe Fix wrong tooltip % in isp pie chart 2022-08-18 13:58:07 +02:00
nymkappa
0243769a02 Improve error logging in ln import 2022-08-18 11:14:34 +02:00
nymkappa
57e0980134 Import json topology 2022-08-18 11:06:20 +02:00
nymkappa
c37b4cadb1 Wrap LN importer into try/catch 2022-08-18 07:48:58 +02:00
nymkappa
350aedd934 Create top 100 oldest nodes full page 2022-08-17 21:29:04 +02:00
nymkappa
9c8fd6431e Create node rankings page with 3 different rankings 2022-08-17 21:20:11 +02:00
wiz
50d99634f7 Merge pull request #2306 from mempool/nymkappa/bugfix/stats-import
Refactor LN stats import
2022-08-18 02:35:16 +09:00
wiz
b1e8d0aab6 Merge branch 'master' into nymkappa/bugfix/stats-import 2022-08-18 00:35:48 +09:00
wiz
86e5048566 Merge pull request #2305 from mempool/nymkappa/bugfix/isp-chart-fix
Refactor ISP pie chart to make it more consitent
2022-08-18 00:35:27 +09:00
nymkappa
6421bc82f2 Create top 100 node per channel count component 2022-08-17 16:19:01 +02:00
nymkappa
2359e44b16 Create top 100 node per capacity component 2022-08-17 16:00:30 +02:00
nymkappa
7520e3beba Refactor top nodes widgets 2022-08-17 12:53:26 +02:00
nymkappa
44cf47b86d Fix node channel list pagination 2022-08-17 10:23:14 +02:00
nymkappa
8dc41257ce Remove xml parser - Read only .topology file and assume json format 2022-08-16 21:47:52 +02:00
nymkappa
a71262f538 Assume topology file are in .json - trim log 2022-08-16 19:29:00 +02:00
nymkappa
264ce1222a Remove "invalid data skipping fix" from stats importer 2022-08-16 19:00:08 +02:00
nymkappa
82f8bf6bb4 Refactor ISP pie chart to make it more consitent 2022-08-16 18:47:45 +02:00
Mononaut
54451c9a8c Fix sticky error state on block page 2022-08-16 16:35:24 +00:00
wiz
7f48416dc3 Merge pull request #2301 from mononaut/tx-unfurls
Add basic transaction link previews
2022-08-16 14:12:37 +09:00
Mononaut
e0ea47b8ee Add basic transaction link previews 2022-08-15 23:14:34 +00:00
wiz
a6b1d4059f Merge pull request #2299 from mempool/simon/translators-size-fix
Fix wrong size of translators avatars
2022-08-15 11:32:04 +09:00
softsimon
238008010d Fix wrong size of translators avatars 2022-08-14 13:24:54 +04:00
wiz
c895bc2681 Merge pull request #2184 from mempool/dependabot/npm_and_yarn/backend/bitcoinjs-lib-6.0.2
Bump bitcoinjs-lib from 6.0.1 to 6.0.2 in /backend
2022-08-14 14:27:10 +09:00
wiz
3684ee1e6c Merge branch 'master' into dependabot/npm_and_yarn/backend/bitcoinjs-lib-6.0.2 2022-08-14 13:51:53 +09:00
wiz
097a763e6e Merge pull request #2283 from mononaut/lightning-unfurls
Lightning unfurls
2022-08-14 13:11:46 +09:00
Mononaut
50d39295a5 Update error handling for lightning previews 2022-08-13 15:32:41 +00:00
Mononaut
d667b8d455 tweak lightning unfurl layouts 2022-08-13 15:32:40 +00:00
Mononaut
9216936a71 Add lightning channel link previews 2022-08-13 15:32:40 +00:00
Mononaut
18d18fa234 Add lightning node link previews 2022-08-13 15:32:36 +00:00
wiz
67ce4a956f Merge pull request #2284 from mempool/simon/remove-sponsor
Removing sponsor. Link to Enterprise sponsor
2022-08-13 20:59:55 +09:00
wiz
ca51d15f86 Hide the Enterprise Sponsor button for now 2022-08-13 20:47:28 +09:00
wiz
d0dd78c47c Merge branch 'master' into simon/remove-sponsor 2022-08-13 20:16:40 +09:00
wiz
c0773afb68 Merge pull request #2289 from knorrium/knorrium/fix_docker_startup_vars
Fix the latest backend Docker startup vars
2022-08-13 20:16:33 +09:00
wiz
8200d54c20 Merge branch 'master' into simon/remove-sponsor 2022-08-13 20:16:17 +09:00
dependabot[bot]
53bc616b1b Bump bitcoinjs-lib from 6.0.1 to 6.0.2 in /backend
Bumps [bitcoinjs-lib](https://github.com/bitcoinjs/bitcoinjs-lib) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/bitcoinjs/bitcoinjs-lib/releases)
- [Changelog](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bitcoinjs/bitcoinjs-lib/compare/v6.0.1...v6.0.2)

---
updated-dependencies:
- dependency-name: bitcoinjs-lib
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-13 11:05:43 +00:00
wiz
b9b1265b78 Merge branch 'master' into knorrium/fix_docker_startup_vars 2022-08-13 20:04:58 +09:00
wiz
927a54e4d9 Merge pull request #2019 from knorrium/knorrium/backend_unit_tests
Start adding unit tests to the backend
2022-08-13 20:04:45 +09:00
wiz
f855173b9b Merge branch 'master' into knorrium/backend_unit_tests 2022-08-13 19:54:10 +09:00
wiz
f618f12515 Merge pull request #2281 from knorrium/knorrium/fix_docker_gha_oom
Fix the OOM issue when building the frontend docker image for armv7
2022-08-13 19:53:45 +09:00
wiz
d902e2b838 Merge branch 'master' into knorrium/fix_docker_gha_oom 2022-08-13 19:47:36 +09:00
wiz
80f4a98ea7 Merge pull request #2295 from mempool/nymkappa/feature/show-more-mining-pool-pie
Reduce pool ranking pie chart min slice
2022-08-13 19:47:13 +09:00
wiz
b870cd69de Merge branch 'master' into nymkappa/feature/show-more-mining-pool-pie 2022-08-13 19:35:31 +09:00
wiz
0f4b11e455 Merge pull request #2294 from mempool/nymkappa/feature/split-node-sockets
Create and populate nodes_socket table
2022-08-13 19:35:24 +09:00
nymkappa
ebb119aa90 Add 'websocket' type to nodes sockets 2022-08-13 11:01:46 +02:00
nymkappa
3e9543f0b6 Create and populate nodes_socket table 2022-08-13 11:01:46 +02:00
wiz
f55cbe11af Merge pull request #2247 from mononaut/gotta-unfurl-fast
Faster unfurls
2022-08-13 17:44:22 +09:00
nymkappa
bd6831fd48 Reduce pool ranking pie chart min slice 2022-08-13 10:40:46 +02:00
wiz
e60bd52e8b Merge branch 'master' into gotta-unfurl-fast 2022-08-13 16:25:04 +09:00
Felipe Knorr Kuhn
ce1a8053c8 Fix the latest backend Docker startup vars 2022-08-12 23:08:05 -07:00
Felipe Knorr Kuhn
fad66c7266 Update tests to include the POOLS_JSON_TREE_URL and POOLS_JSON_URL fields 2022-08-12 22:53:37 -07:00
Felipe Knorr Kuhn
240394817f Merge branch 'master' into knorrium/backend_unit_tests 2022-08-12 22:46:35 -07:00
Felipe Knorr Kuhn
d037ae2faf Update config unit tests 2022-08-12 22:44:13 -07:00
Felipe Knorr Kuhn
53d1497ce8 Update backend config fixture to include automatic block reindexing 2022-08-12 22:44:05 -07:00
Felipe Knorr Kuhn
b7a8ac8343 Fix merge conflicts on package-lock 2022-08-12 22:37:15 -07:00
Felipe Knorr Kuhn
9a3929554b Fix merge conflicts on gitignore 2022-08-12 22:35:50 -07:00
Felipe Knorr Kuhn
b082d81924 Merge branch 'master' into knorrium/fix_docker_gha_oom 2022-08-12 20:33:32 -07:00
Felipe Knorr Kuhn
927141fb13 Remove dependency on TTP GHA 2022-08-12 20:31:59 -07:00
Mononaut
1a903d3efb Unbork unfurler concurrency implementation 2022-08-12 16:49:56 +00:00
Mononaut
0631f357b6 Improve unfurler client-side error handling 2022-08-12 16:49:43 +00:00
wiz
04242b686e Merge pull request #2279 from hunicus/remove-link-cpfp
Avoid linking cpfp link api examples
2022-08-13 00:21:17 +09:00
wiz
b0015dfe5c Merge branch 'master' into remove-link-cpfp 2022-08-13 00:10:36 +09:00
wiz
2fc15f3be8 Merge pull request #2287 from mempool/nymkappa/feature/update-ln-dashboard
Show two LN network stats charts on dashboard
2022-08-13 00:10:00 +09:00
wiz
8edbd60bbf Merge branch 'master' into nymkappa/feature/update-ln-dashboard 2022-08-12 23:52:35 +09:00
wiz
4249270e30 Merge pull request #2286 from mempool/nymkappa/bugfix/lnd-node-networks
Fix LN stats node per network count
2022-08-12 23:52:19 +09:00
wiz
ffe8e52095 Merge branch 'master' into nymkappa/bugfix/lnd-node-networks 2022-08-12 23:43:26 +09:00
nymkappa
0c13d9f585 Update yaxis scale and view more link 2022-08-12 15:40:19 +02:00
nymkappa
39549c8ca9 Show two LN network stats charts on dashboard 2022-08-12 15:35:00 +02:00
wiz
54355b61d6 Merge branch 'master' into gotta-unfurl-fast 2022-08-12 21:30:11 +09:00
wiz
ed2f1b4603 Merge pull request #2285 from mempool/simon/matomo-networks-fix
Fixing broken Matomo och bisq and liquid
2022-08-12 21:26:56 +09:00
softsimon
576400414f Adding staging matomo sites 2022-08-12 16:11:38 +04:00
nymkappa
06453edc7c Fix LN stats node per network count 2022-08-12 12:12:34 +02:00
softsimon
329e9aa034 Fixing broken Matomo och bisq and liquid 2022-08-12 13:27:43 +04:00
wiz
6cba4a8492 Merge pull request #2282 from mempool/nymkappa/bugfix/remove-extra-call-leak
Fix recursive call in LN network updater
2022-08-12 13:46:39 +09:00
wiz
4f1ebc2545 Merge branch 'master' into nymkappa/bugfix/remove-extra-call-leak 2022-08-12 11:37:31 +09:00
softsimon
1008ab6222 Removing sponsor. Link to Enterprise sponsor 2022-08-12 03:44:58 +04:00
Mononaut
bbf04648f9 Handle missing blocks/addresses in preview 2022-08-11 17:43:28 +00:00
Mononaut
31ced9e23c don't use puppeteer to render unfurl meta tags 2022-08-11 17:43:27 +00:00
Mononaut
94d1aeb287 Fix unfurler language support 2022-08-11 17:43:26 +00:00
Mononaut
06f232fdd8 handle SIGTERM gracefully in unfurler 2022-08-11 17:43:26 +00:00
Mononaut
e4342113fa Improve unfurl layout & resize to 1200x600 2022-08-11 17:43:25 +00:00
Mononaut
578a1b6d19 Speed up unfurls by reusing puppeteer sessions 2022-08-11 17:43:24 +00:00
softsimon
251de2be11 Merge pull request #2280 from hunicus/remove-listener
Remove doc scroll listener after navigating away
2022-08-11 21:23:40 +04:00
hunicus
786cd85c74 Avoid linking cpfp link api examples 2022-08-11 12:43:30 -04:00
nymkappa
7e356ef0a0 Fix recursive call in LN network updater 2022-08-11 16:51:09 +02:00
wiz
e48eab403c Merge pull request #2274 from mempool/nymkappa/bugfix/channel-map-disable-highlight
Disable country highlight - Update node styling
2022-08-11 21:38:12 +09:00
wiz
e0c61f7299 Merge branch 'master' into remove-listener 2022-08-11 20:42:12 +09:00
wiz
b24256a83e Merge pull request #2278 from hunicus/nofollow-api-links
Add nofollow to all api link examples
2022-08-11 20:41:11 +09:00
wiz
631de8d85f Merge branch 'master' into nofollow-api-links 2022-08-11 20:09:12 +09:00
wiz
c1ebbc556b Merge pull request #2275 from mempool/nymkappa/bugfix/channel-map-rendering
Fix channel rendering issue
2022-08-11 20:08:44 +09:00
wiz
73285ae776 Merge branch 'master' into nymkappa/bugfix/channel-map-rendering 2022-08-11 19:47:34 +09:00
wiz
2d692c3f20 Merge pull request #2265 from mempool/nymkappa/bugfix/clightning-wrong-channel-id
Convert short_id to integer id with clightning backend before returning the graph
2022-08-11 19:47:19 +09:00
wiz
0f4b127275 Merge branch 'master' into nymkappa/bugfix/channel-map-rendering 2022-08-11 18:49:01 +09:00
wiz
c85b6d53c5 Merge branch 'master' into nymkappa/bugfix/clightning-wrong-channel-id 2022-08-11 18:38:11 +09:00
wiz
8ed1644081 Merge pull request #2277 from mempool/nymkappa/feature/update-dashboard
Add ISP chart in the dashboard - Fix mobile layout - Start polishing
2022-08-11 18:12:03 +09:00
nymkappa
1d71e26a12 Add ISP chart in the dashboard - Fix mobile layout - Start polishing 2022-08-11 10:19:13 +02:00
Felipe Knorr Kuhn
bb2e4a4fb3 Fix the OOM issue when building the frontend docker image for armv7 2022-08-10 22:11:49 -07:00
hunicus
5b4d394039 Remove doc scroll listener after navigating away 2022-08-10 15:17:47 -04:00
hunicus
2aaa392bf5 Add nofollow to all api link examples 2022-08-10 12:33:22 -04:00
nymkappa
8919cbcdc1 Fix channel rendering issue 2022-08-10 17:03:11 +02:00
nymkappa
dc7231537f Refactor channel id conversion utils 2022-08-10 16:58:29 +02:00
nymkappa
b31a82d27e Disable country highlight - Update node styling 2022-08-10 16:42:01 +02:00
wiz
7fecea9cca Merge pull request #2273 from mempool/nymkappa/feature/channel-page-map
Show channel on the map in channel page
2022-08-10 23:24:31 +09:00
wiz
b0d4c9eac8 Merge branch 'master' into nymkappa/feature/channel-page-map 2022-08-10 23:10:44 +09:00
nymkappa
db41aed44b Show channel on the map in channel page 2022-08-10 16:00:12 +02:00
wiz
ff370684f1 Merge pull request #2268 from mempool/nymkappa/feature/config-pools-json
Make mining pools url configurable
2022-08-10 22:52:26 +09:00
wiz
8c21cc56d4 Merge branch 'master' into nymkappa/feature/config-pools-json 2022-08-10 22:40:04 +09:00
wiz
49d8b3bacd Merge pull request #2262 from Emzy/ops/add-core-lightning
Add Core Lighting for FreeBSD in prod installer
2022-08-10 22:39:19 +09:00
wiz
e0d677b01c Merge branch 'master' into ops/add-core-lightning 2022-08-10 22:38:36 +09:00
wiz
2b2c40f65a [ops] Fix minor issue for /cln/.zshrc in prod installer 2022-08-10 22:37:22 +09:00
wiz
4e61f1ff36 Merge pull request #2086 from mempool/nymkappa/bugfix/index-blocks-prices-often
Missing blocks prices
2022-08-10 22:34:20 +09:00
wiz
33cf69872b Merge branch 'master' into nymkappa/bugfix/index-blocks-prices-often 2022-08-10 22:23:35 +09:00
nymkappa
48a0a6c7e3 Convert short_id to integer id with clightning backend before returning the graph 2022-08-10 15:09:34 +02:00
wiz
7012a480e8 Merge pull request #2267 from mempool/nymkappa/feature/node-status
Add `nodes.status` field
2022-08-10 22:00:50 +09:00
wiz
88e6305b9a Merge branch 'master' into nymkappa/feature/node-status 2022-08-10 21:26:07 +09:00
wiz
b479136688 Merge pull request #2270 from mempool/nymkappa/feature/rewrite-map-component
Rewrite channels map component using native echart
2022-08-10 19:46:13 +09:00
nymkappa
d6a42cdf6b Rewrite channels map component using native echart 2022-08-10 11:28:54 +02:00
nymkappa
aed37afb3e Add missing config in docker script and sample 2022-08-09 17:26:14 +02:00
nymkappa
a64cb4bbad Make mining pools url configurable 2022-08-09 15:52:24 +02:00
nymkappa
9b974dfbd9 Add nodes.status db field to mark nodes as inactive if needed 2022-08-09 11:17:37 +02:00
nymkappa
61e512b8f7 Refactor the LN backend and add more logs 2022-08-09 11:12:05 +02:00
nymkappa
2a6f48d8c8 Handle core timeout during closed channel scan, using correct config variable 2022-08-09 11:07:13 +02:00
nymkappa
6a52725b63 Make sure we work with integer in the stats importer 2022-08-09 10:28:40 +02:00
nymkappa
abb078f7ee Convert to short_id before fetching the funding tx 2022-08-09 09:21:31 +02:00
nymkappa
47363b477e Refactor the LN backend and add more logs 2022-08-09 09:20:25 +02:00
wiz
c0e6b7af58 Merge pull request #2251 from mempool/nymkappa/bugfix/clightning-crash
Don't throw an exception when cln connection is down
2022-08-08 17:28:50 +09:00
wiz
bd822998a5 Merge pull request #2260 from mempool/nymkappa/bugfix/missing-last-data-charts
Don't insert stats in the future
2022-08-08 17:10:59 +09:00
wiz
771d21e410 Merge branch 'master' into nymkappa/bugfix/missing-last-data-charts 2022-08-08 15:59:34 +09:00
wiz
3b1d4ffe43 Merge pull request #2259 from mempool/nymkappa/bugfix/unique-lightning-stats
Make sure lightning stats are not duplicated in db
2022-08-08 15:58:59 +09:00
wiz
78d1ef9b1c Merge branch 'master' into nymkappa/bugfix/unique-lightning-stats 2022-08-08 15:40:46 +09:00
wiz
632da54146 Merge pull request #2252 from mempool/nymkappa/bugfix/cln-channel-id
Fix CLN channel short_id conversion
2022-08-08 15:40:28 +09:00
wiz
b725fc8d26 Merge branch 'master' into nymkappa/bugfix/cln-channel-id 2022-08-08 15:25:20 +09:00
wiz
03c6c0567a Merge pull request #2264 from mempool/simon/right-align-logo
Right align mempool logo on mobile with dual logos
2022-08-08 15:25:00 +09:00
nymkappa
1d106a9851 Fix CLN channel short_id conversion 2022-08-08 07:50:50 +02:00
softsimon
5612a033d5 Right align mempool logo on mobile with dual logos 2022-08-06 04:25:21 +04:00
Stephan Oeste
cacd4abd9d Add Core Lighting for FreeBSD in prod installer 2022-08-05 16:41:00 +02:00
nymkappa
c01c610bb3 Don't insert stats in the future 2022-08-05 12:43:26 +02:00
wiz
82bcac7c74 Merge pull request #2254 from mempool/nymkappa/feature/channels-map-node-page 2022-08-05 10:37:34 +00:00
nymkappa
5a50a0d973 Make sure lightning stats are not duplicated in db 2022-08-05 12:32:20 +02:00
nymkappa
5d81a13a80 Always show channels map in node page - auto zoom on the node 2022-08-05 10:11:29 +02:00
wiz
0f9941f0d1 Merge pull request #2250 from mempool/nymkappa/bugfix/missing-data-node-page
Fix node page and display real time data
2022-08-05 08:05:15 +00:00
wiz
f7cbe30a16 Merge branch 'master' into nymkappa/bugfix/missing-data-node-page 2022-08-05 07:55:11 +00:00
wiz
76f261eb38 Merge pull request #2255 from mempool/nymkappa/feature/update-node-stats-often
Update latest stats every 10 minutes
2022-08-05 07:54:14 +00:00
wiz
fad73cf3f5 Merge branch 'master' into nymkappa/feature/update-node-stats-often 2022-08-05 07:42:36 +00:00
wiz
6796bb94cc Merge pull request #2244 from mempool/nymkappa/bugfix/daily-stats-crash
Fix daily LN stats crash
2022-08-05 07:42:25 +00:00
nymkappa
54669281de Run node stats every 10 minutes, only keep the latest entry per day 2022-08-04 18:27:36 +02:00
nymkappa
d647edcae3 Re-applied missing fix from https://github.com/mempool/mempool/pull/2233 2022-08-04 17:50:49 +02:00
softsimon
ab985afe01 Merge pull request #2253 from mempool/nymkappa/bugfix/fix-reverted-code
Re-applied missing fix from https://github.com/mempool/mempool/pull/2233
2022-08-04 13:24:57 +02:00
nymkappa
f60ef05223 Re-applied missing fix from https://github.com/mempool/mempool/pull/2233 2022-08-04 13:11:24 +02:00
nymkappa
f6d6ea5d31 Gracefully attempt to reconnect to cln upon error 2022-08-04 13:05:15 +02:00
nymkappa
3bf7bf5563 Add missing file 2022-08-04 12:49:07 +02:00
nymkappa
3c2e27f778 Fix node page and display real time data 2022-08-04 11:30:32 +02:00
nymkappa
99379d53bf When LN backend crashed, catch the error and restart after 1 minute 2022-08-03 12:43:41 +02:00
nymkappa
6be2985b40 Fix daily LN stats crash 2022-08-03 12:13:55 +02:00
wiz
faa59f59bd Merge pull request #2236 from mempool/wiz/fix-syslog-perms
[ops] Fix syslog permissions for /var/log/mempool
2022-08-03 00:24:10 +00:00
wiz
5147b0dbc4 Merge pull request #2237 from mempool/ops/fix-liquid-assets-sync
[ops] Fix cron jobs to update liquid assets hourly
2022-08-03 00:24:00 +00:00
wiz
db779578d2 Merge pull request #2241 from mempool/nymkappa/bugfix/top-nodes-queries
Rewrite queries to get top nodes by channels and capacity
2022-08-03 00:23:50 +00:00
wiz
76600af698 Merge branch 'master' into nymkappa/bugfix/top-nodes-queries 2022-08-02 23:53:03 +00:00
wiz
a43a65df2c Merge pull request #2240 from mempool/nymkappa/feature/clightning
Add clightning support to the lightning backend
2022-08-02 23:52:53 +00:00
wiz
215df5efed Merge branch 'master' into nymkappa/feature/clightning 2022-08-02 22:57:25 +00:00
wiz
feda827860 Merge pull request #2231 from mempool/nymkappa/feature/ln-historical-import
Import LN historical statistics (network wide + per node)
2022-08-02 22:56:48 +00:00
wiz
33f3b0006b Move fast-xml-parser from devDeps to deps 2022-08-02 21:49:53 +02:00
nymkappa
a25af16f7c Fetch funding tx for clightning channels 2022-08-02 18:34:20 +02:00
nymkappa
00cd3ee9bf Don't run the ln network update if the graph is emtpy 2022-08-02 18:34:19 +02:00
nymkappa
80f1ee45b5 Rebased using the update lightning interfaces 2022-08-02 18:34:19 +02:00
nymkappa
eb90434c28 Delete historical generation code 2022-08-02 18:34:19 +02:00
nymkappa
a94403b3a1 Wrote some utility functions to convert clightning output to our db schema 2022-08-02 18:34:19 +02:00
nymkappa
3f83e517f0 Create CLightningClient class 2022-08-02 18:34:18 +02:00
nymkappa
82cef095fc Rewrite queries to get top nodes by channels and capacity 2022-08-02 18:26:07 +02:00
nymkappa
b6ba3c5781 Ignore channels fee rate > 5000ppm or base fee > 5000 in stats 2022-08-02 18:15:34 +02:00
nymkappa
5b521cfc7c Don't insert gapped gossip data upon restart 2022-08-02 17:56:46 +02:00
nymkappa
d7f2f4136c Small cleanup 2022-08-02 16:00:40 +02:00
nymkappa
5d7e42195f Reduce massive gaps in the imported historical LN data 2022-08-02 16:00:40 +02:00
nymkappa
7fdf95ad34 Remove buggy tx vout value fetching and improve performances 2022-08-02 16:00:40 +02:00
nymkappa
5287490894 Make sure to not count channels twice 2022-08-02 16:00:40 +02:00
nymkappa
b246c6f4c3 We don't need a synced node to import historical data 2022-08-02 16:00:39 +02:00
nymkappa
2daf94f65a Re-use LN stats importer code to log daily LN stats 2022-08-02 16:00:39 +02:00
nymkappa
91ada9ce75 Integrate LN stats importer into the main process 2022-08-02 16:00:39 +02:00
nymkappa
4ea1e98547 Import LN historical statistics (network wide + per node) 2022-08-02 16:00:38 +02:00
wiz
82f9814438 [ops] Fix cron jobs to update liquid assets hourly 2022-08-02 01:00:06 +02:00
wiz
b8c82c8f2c [ops] Fix syslog permissions for /var/log/mempool 2022-08-02 00:08:02 +02:00
wiz
55966e601a Merge pull request #2235 from mempool/simon/limit-matomo
Limit matomo to mempool.space
2022-08-01 18:26:58 +00:00
wiz
886498fe01 Merge pull request #2234 from mempool/nymkappa/bugfix/world-map-ux
Fix UX interaction with channels map
2022-08-01 18:25:05 +00:00
softsimon
54eb11a2a7 Limit matomo to mempool.space 2022-08-01 20:08:53 +02:00
wiz
73c9ec20e8 Merge branch 'master' into nymkappa/bugfix/world-map-ux 2022-08-01 17:50:46 +00:00
wiz
c4f125b2d8 Merge pull request #2233 from mempool/nymkappa/bugfix/missing-alias-fallback-pubkey
Set default values when pubkey, capacity and channels are missing from top nodes
2022-08-01 17:50:37 +00:00
wiz
03dfca3bee Merge branch 'master' into nymkappa/bugfix/missing-alias-fallback-pubkey 2022-08-01 17:41:33 +00:00
nymkappa
04d7265a86 Fix UX interaction with channels map 2022-08-01 18:55:35 +02:00
nymkappa
4335ee8157 Set default values when pubkey, capacity and channels are missing from top nodes 2022-08-01 18:41:31 +02:00
wiz
6ba8b0ec58 Merge pull request #2227 from mempool/nymkappa/bugfix/re-enable-ln-map-click
Re-enabled channels world map click event
2022-08-01 16:35:06 +00:00
wiz
fc463a9561 Merge branch 'master' into nymkappa/bugfix/re-enable-ln-map-click 2022-08-01 16:19:01 +00:00
wiz
a80c79bd70 Merge pull request #2226 from mempool/nymkappa/bugfix/node-null-link
Fix missing pub key, capacity and channel count for node lists
2022-08-01 16:17:52 +00:00
wiz
3fd8af0c78 Merge branch 'master' into nymkappa/bugfix/node-null-link 2022-08-01 15:33:47 +00:00
wiz
474f2d2e7d Merge pull request #2221 from antonilol/lnd-rest-api
use lnd rest api
2022-08-01 15:32:15 +00:00
nymkappa
b61ef6814b Re-enabled channels world map click event 2022-08-01 10:07:15 +02:00
nymkappa
f4670156a5 Fix missing pub key, capacity and channel count for node lists 2022-08-01 09:59:20 +02:00
wiz
2a3111af6d Merge branch 'master' into lnd-rest-api 2022-07-31 22:01:50 +00:00
wiz
573168f647 Merge pull request #2224 from mempool/simon/redirect-with-path
Redirect with path
2022-07-31 22:01:35 +00:00
softsimon
7570603b37 Redirect with path 2022-07-31 23:25:28 +02:00
Antoni Spaanderman
887fb13f34 use lnd rest api 2022-07-30 21:52:58 +02:00
wiz
1a761e79ad Merge pull request #2222 from Emzy/ops/fix-tor-freebsd
Fix tor config for FreeBSD on prod installer
2022-07-30 13:35:26 +00:00
wiz
8d1624476f Remove TOR_HOME variable in prod/install 2022-07-30 15:32:51 +02:00
wiz
8f183945c0 Fix FreeBSD path for torrc 2022-07-30 15:30:55 +02:00
wiz
5d704b0e43 Merge branch 'master' into ops/fix-tor-freebsd 2022-07-30 13:29:19 +00:00
wiz
0c6ceaefa2 Merge pull request #2220 from Emzy/ops/add-unfurl-install
Add Unfurl to the prod installer
2022-07-30 13:29:11 +00:00
wiz
a413c6ebb8 Separate electrs into bitcoin electrs and elements electrs 2022-07-30 15:25:16 +02:00
wiz
2e891eb926 Merge branch 'master' into ops/add-unfurl-install 2022-07-30 13:14:02 +00:00
Stephan Oeste
21b6c6158a Fix tor config for FreeBSD on prod installer 2022-07-30 14:01:40 +02:00
Stephan Oeste
eaf7da9acb Add Unfurl to the prod installer 2022-07-29 20:13:48 +02:00
wiz
63a22082bc Merge pull request #2217 from Emzy/ops/pats-dialog
Add options for components to be installed in prod install script
2022-07-29 16:31:53 +00:00
Stephan Oeste
6e38caee63 Add options for components to be installed in prod install script 2022-07-29 13:08:07 +02:00
wiz
cfa2690549 Merge pull request #2212 from mempool/nymkappa/feature/node-network-stack
Convert nodes per network chart to stack style
2022-07-28 23:26:59 +00:00
wiz
c097db2c3c Merge branch 'master' into nymkappa/feature/node-network-stack 2022-07-28 23:17:24 +00:00
wiz
2a96d3d213 Merge pull request #2209 from mempool/nymkappa/bugfix/remove-dead-view-more-links
Remove useless view more links that does not link to anywhere
2022-07-28 23:17:12 +00:00
wiz
01a5748c02 Merge branch 'master' into nymkappa/bugfix/remove-dead-view-more-links 2022-07-28 23:16:39 +00:00
wiz
61e8e2fb2a Merge pull request #2208 from mempool/nymkappa/feature/ln-dashboard-map-click
Update mouse UX on LN map in dashboard
2022-07-28 23:16:24 +00:00
wiz
d7fe92765c Merge branch 'master' into nymkappa/feature/ln-dashboard-map-click 2022-07-28 22:23:31 +00:00
wiz
bc0c3a1eed Merge pull request #2207 from mempool/nymkappa/bugfix/mobile-menus
Reduce mobile menus padding
2022-07-28 22:23:21 +00:00
nymkappa
98e5f78d5f Convert nodes per network chart to stack style 2022-07-28 10:01:04 +02:00
nymkappa
6714533ed4 Remove useless view more links that does not link to anywhere 2022-07-28 07:50:58 +02:00
nymkappa
94a536af28 Update mouse UX on LN map in dashboard (wip) 2022-07-28 07:45:37 +02:00
nymkappa
c3780adab2 Reduce mobile menus padding 2022-07-28 06:52:08 +02:00
wiz
bdb76b3d4b Merge pull request #2206 from mononaut/address-unfurls
Address unfurls
2022-07-28 01:59:29 +00:00
wiz
80ee890a9f Merge branch 'master' into address-unfurls 2022-07-28 01:48:59 +00:00
wiz
b0a232806b Merge pull request #2205 from mononaut/better-block-unfurls
Better block unfurls
2022-07-28 01:48:36 +00:00
wiz
9c44bf171c Merge branch 'master' into better-block-unfurls 2022-07-28 01:36:10 +00:00
wiz
5d88dfd00b Merge pull request #2173 from mononaut/unfurl-daemon
Open Graph link unfurler service
2022-07-28 01:35:58 +00:00
wiz
fc14dc95df Update git URL in unfurl/package.json 2022-07-28 03:34:14 +02:00
wiz
51bdbc5d4a Add linux deps to unfurler README 2022-07-28 03:32:08 +02:00
wiz
b2e6573743 Merge pull request #2181 from oleonardolima/feature/add-get-block-raw-route
feature: add /block/:hash/raw api route
2022-07-27 22:47:03 +00:00
wiz
d66cc8a213 Merge branch 'master' into feature/add-get-block-raw-route 2022-07-27 22:35:22 +00:00
wiz
a6663d7869 Merge pull request #2204 from mempool/nymkappa/bugfix/silence-ln-db-migration
Silence LN db migration is CONFIG.LIGHTNING.ENABLED = false
2022-07-27 22:35:02 +00:00
softsimon
b0fe879503 Update backend/src/api/bitcoin/bitcoin.routes.ts 2022-07-27 23:33:18 +02:00
nymkappa
f610699ef4 Remove useless notice message content 2022-07-27 23:01:57 +02:00
nymkappa
d2ece2993e Silence LN db truncation messages is CONFIG.LIGHTNING.ENABLED = false 2022-07-27 22:53:09 +02:00
Leonardo Lima
a9248a5f13 feat: parse rpc full block from hex to binary representation 2022-07-27 17:12:33 -03:00
wiz
3e2cf5c058 Merge pull request #2203 from mempool/nymkappa/bugfix/channels-update-status
Don't set all channels to inactive when the updater runs
2022-07-27 19:16:54 +00:00
wiz
48496abbf4 Merge branch 'master' into nymkappa/bugfix/channels-update-status 2022-07-27 18:53:29 +00:00
wiz
88648da890 Merge pull request #2202 from mempool/nymkappa/bugfix/only-user-active-channel-for-stats
Ony consider channel stats = 1 for stats calculation
2022-07-27 18:53:18 +00:00
wiz
929491ce3d Merge pull request #2201 from mempool/nymkappa/feature/ln-pie-charts-toggle
Add capacity/nodes, include/exclude Tor from ISP chart
2022-07-27 18:40:06 +00:00
wiz
512739ae90 Merge branch 'master' into nymkappa/feature/ln-pie-charts-toggle 2022-07-27 18:26:06 +00:00
wiz
69930ef698 Merge pull request #2200 from mempool/nymkappa/feature/create-toggle-component
Create shared toggle component to re-use
2022-07-27 18:25:41 +00:00
Mononaut
5854931430 Add address link previews 2022-07-27 18:13:37 +00:00
nymkappa
f57fa1286c Don't mark closed channels as inactive 2022-07-27 17:24:31 +02:00
nymkappa
edfa0d6074 Don't set all channels to inactive when the updater runs 2022-07-27 17:21:24 +02:00
nymkappa
d6e9500bee [LN stats] Ony consider channel stats = 1 for stats calculation 2022-07-27 16:19:47 +02:00
nymkappa
f30883a018 [LN ISP chart] Only show "Tor node excluded" when appropriate 2022-07-27 15:33:25 +02:00
nymkappa
12eea0e4cc [LN ISP chart] Adds toogle to order by nodes/capacity and show/hide Tor 2022-07-27 13:20:54 +02:00
nymkappa
16db740986 Create shared toggle component to re-use 2022-07-27 10:48:27 +02:00
Mononaut
d1ad9efe64 Add open graph link titles 2022-07-27 01:26:14 +00:00
wiz
1b3faa1203 Merge pull request #2183 from mempool/simon/new-eslint-rules
A couple of new eslint rules
2022-07-26 23:16:22 +00:00
wiz
ce9a4024b3 Merge branch 'master' into simon/new-eslint-rules 2022-07-26 23:15:52 +00:00
wiz
16e79a3662 Merge pull request #2192 from mempool/simon/logo-vertical-center-fix
Fix for mempool logo not being centered vertically
2022-07-26 23:15:41 +00:00
Mononaut
a67c0b166c Improve block link preview legibility 2022-07-26 23:10:21 +00:00
Mononaut
fbf15f05ed Add block link previews for other networks 2022-07-26 23:09:37 +00:00
Mononaut
d1e2ead13e Set opengraph tags directly in the front end 2022-07-26 23:09:34 +00:00
wiz
9bf04b0af4 Merge branch 'master' into simon/logo-vertical-center-fix 2022-07-26 23:06:58 +00:00
wiz
d6ff8753e2 Merge pull request #2190 from mempool/nymkappa/feature/order-by-date-not-by-id
[LN stats] Order lightning_stats by added timestamp instead of id
2022-07-26 23:06:47 +00:00
wiz
3207e2a285 Merge pull request #2193 from Emzy/ops/mysql-pw
Add random generated mysql passwords on prod install
2022-07-26 22:57:43 +00:00
wiz
a30808a972 Merge pull request #2194 from mempool/ops/fix-prod-bitcoin-conf-zmq
Fix zmq ports in prod bitcoin.conf
2022-07-26 22:55:32 +00:00
wiz
a49cb2d611 Merge branch 'master' into nymkappa/feature/order-by-date-not-by-id 2022-07-26 22:54:31 +00:00
wiz
7933a51994 Merge pull request #2187 from mempool/nymkappa/feature/timespan-dashboard
[LN Dashboard] - Show 3y charts instead of 1y
2022-07-26 22:54:21 +00:00
wiz
fbef2157ec Merge branch 'master' into nymkappa/feature/timespan-dashboard 2022-07-26 22:53:50 +00:00
wiz
b2321495d8 Merge pull request #2186 from mempool/nymkappa/bugfix/remove-1mb-node-capacity
[Node page] Remove node chart 1mb line and fix y axis
2022-07-26 22:53:36 +00:00
wiz
7e504e783f Fix zmq ports in prod bitcoin.conf 2022-07-27 00:47:46 +02:00
wiz
a14565e288 Merge branch 'master' into nymkappa/bugfix/remove-1mb-node-capacity 2022-07-26 22:44:54 +00:00
wiz
9e6dd65e57 Merge pull request #2185 from mempool/nymkappa/feature/link-country-isp-node-page
[Node page] Add link to node list per country/isp in node page
2022-07-26 22:43:30 +00:00
wiz
898fef19cc Merge branch 'master' into nymkappa/feature/link-country-isp-node-page 2022-07-26 21:01:56 +00:00
wiz
a9469f7e2b Merge pull request #2188 from antonilol/taproot-fee-tooltip-2
Address @Xekyo's comments on #2167
2022-07-26 21:01:30 +00:00
Stephan Oeste
412a0ee577 Add random generated mysql passwords on prod install 2022-07-26 22:07:46 +02:00
softsimon
d8b3c21a6c Fix for mempool logo not being centered vertically 2022-07-26 21:35:19 +02:00
nymkappa
68f288f69c Order lightning_stats by added timestamp instead of id 2022-07-26 17:32:43 +02:00
Antoni Spaanderman
3ba37aaa5a Address @Xekyo's comments on https://github.com/mempool/mempool/pull/2167
+ minor fixes, variable name consistency and ts types
2022-07-26 16:29:42 +02:00
nymkappa
b69a7a5031 Show 3y charts by default on dashboard to get more interesting charts 2022-07-26 15:18:42 +02:00
nymkappa
2acaa45e0a Remove node chart 1mb line and fix y axis 2022-07-26 14:10:09 +02:00
nymkappa
6341839de4 [Node page] Add link to node list per country/isp in node page 2022-07-26 12:26:44 +02:00
softsimon
bc132e4337 A couple of new eslint rules 2022-07-26 02:38:23 +02:00
Leonardo Lima
46e63ca6cf feat: add /block/:hash/raw api route 2022-07-25 17:08:42 -03:00
Leonardo Lima
6ae05c2023 chore: accept the CLA for @oleonardolima 2022-07-25 17:08:33 -03:00
wiz
65cd708295 Merge pull request #2172 from mempool/simon/jumping-logo-fix
Fix for mempool logo jumping with various sizes of enterprise logo
2022-07-25 00:42:29 +02:00
Mononaut
9656ee92b7 Add Open Graph link unfurler service 2022-07-24 21:16:57 +00:00
softsimon
9ff8487feb Fix for mempool logo jumping with various sizes of enterprise logo 2022-07-24 23:01:31 +02:00
wiz
fbdf6da314 Merge pull request #2167 from antonilol/taproot-fee-tooltip
Add Taproot transaction feature tooltip with fee saving information
2022-07-24 21:51:17 +02:00
Antoni Spaanderman
46bce30a64 add taproot badge with only privacy tooltip if no fees can be saved 2022-07-24 19:39:13 +02:00
Antoni Spaanderman
b875bc2552 Update frontend/src/app/bitcoin.utils.ts
triple equals

Co-authored-by: softsimon <softsimon@users.noreply.github.com>
2022-07-24 18:44:53 +02:00
Antoni Spaanderman
37fd1fb76d move parseMultisigScript to bitcoin.util.ts 2022-07-24 18:44:27 +02:00
wiz
777a1bb4c1 Merge branch 'master' into taproot-fee-tooltip 2022-07-24 17:42:39 +02:00
wiz
b484852a62 Merge pull request #2111 from mempool/translations_frontend-src-locale-messages-xlf--master_pl
Translate '/frontend/src/locale/messages.xlf' in 'pl'
2022-07-24 17:42:16 +02:00
wiz
12e516c366 Merge pull request #2171 from mempool/nymkappa/bugfix/remove-duplicated-node-map
Remove duplicated nodes from the world map
2022-07-24 17:41:29 +02:00
wiz
441a5fa2b4 Merge branch 'master' into nymkappa/bugfix/remove-duplicated-node-map 2022-07-24 16:48:20 +02:00
wiz
a5b502db4a Merge pull request #2170 from mempool/feature/nymkappa/node-does-not-exists
[Node page] Handle non existing node
2022-07-24 16:48:14 +02:00
wiz
0b2b8fc56c Merge branch 'master' into feature/nymkappa/node-does-not-exists 2022-07-24 16:39:32 +02:00
wiz
d3e24914cd Merge pull request #2169 from mempool/nymkappa/feature/empty-channels-list
[Node page] Handle empty channels list
2022-07-24 16:39:23 +02:00
wiz
1438391515 Merge branch 'master' into nymkappa/feature/empty-channels-list 2022-07-24 16:27:06 +02:00
wiz
8316c37a0e Merge pull request #2168 from mempool/nymkappa/feature/node-channels-list-count
[Node page] Update channels count when switching between open/closed
2022-07-24 16:26:39 +02:00
nymkappa
44725d9b29 Remove duplicated nodes from the world map 2022-07-24 15:08:48 +02:00
nymkappa
d9e85fdcb6 Show error message when the node public key does not exist 2022-07-24 12:34:50 +02:00
nymkappa
886e7e6638 [Node page] Show message if there is no channels in the list to display 2022-07-24 12:05:09 +02:00
nymkappa
479f635754 [Node page] Update channels count when switching between open/closed 2022-07-24 11:51:05 +02:00
wiz
75cd5a15b7 Merge branch 'master' into taproot-fee-tooltip 2022-07-24 00:15:32 +02:00
wiz
0f91778970 Merge pull request #2166 from mempool/nymkappa/feature/isp-maxmind
Integrate GeoIP2 ISP database
2022-07-24 00:14:21 +02:00
Antoni Spaanderman
eb9c6f2231 Add Taproot transaction feature tooltip with fee saving information 2022-07-24 00:08:53 +02:00
wiz
ad9e989598 Correct maxmind geoip-db GEOLITE2_ISP to GEOIP2_ISP 2022-07-23 23:53:28 +02:00
nymkappa
ffe22399d5 Integrate GeoIP2 ISP database 2022-07-23 23:33:13 +02:00
wiz
300b9e4e05 Merge pull request #2157 from mempool/nymkappa/feature/ln-map-single-node
Add channels map to the node page
2022-07-23 19:37:29 +02:00
nymkappa
89f7f99720 Rebased on top of master 2022-07-23 19:03:12 +02:00
wiz
b139423eb9 Merge pull request #2155 from mempool/nymkappa/feature/clickable-flags
Make flags clickable
2022-07-23 18:45:40 +02:00
nymkappa
40f2b97075 Add channels map to the node page 2022-07-23 18:43:08 +02:00
wiz
8de89a9f26 Merge branch 'master' into nymkappa/feature/clickable-flags 2022-07-23 18:31:30 +02:00
wiz
33776b2b09 Merge pull request #2158 from mempool/nymkappa/feature/ln-map-dashboard
Show LN map on the LN dashboard
2022-07-23 18:30:18 +02:00
nymkappa
e0d189b70a Remove horizontal scroll on ln dashboad 2022-07-23 16:14:32 +02:00
wiz
2e210e7aa4 Merge pull request #2154 from mempool/nymkappa/bugfix/wrong-i18n-string
Fix wrong i18n key
2022-07-23 16:13:29 +02:00
nymkappa
f96b7e7004 Show LN map on the LN dashboard 2022-07-23 14:23:47 +02:00
nymkappa
0508ac1a5d Make flags clickable 2022-07-23 10:18:36 +02:00
nymkappa
ae34225f66 Fix wrong i18n key 2022-07-23 09:57:30 +02:00
Felipe Knorr Kuhn
c509a69f1d Merge branch 'master' into translations_frontend-src-locale-messages-xlf--master_pl 2022-07-22 21:17:38 -07:00
wiz
6635f2ce8f Merge pull request #2152 from mononaut/open-graph-previews
Draft: Open Graph link previews
2022-07-22 23:04:35 +02:00
Mononaut
37c28940bf Add open graph block preview page 2022-07-22 19:45:59 +00:00
Felipe Knorr Kuhn
51b832335b Merge branch 'master' into nymkappa/bugfix/index-blocks-prices-often 2022-07-22 08:06:44 -07:00
wiz
0aec1a5d68 Merge pull request #2150 from knorrium/knorrium/refactor_cypress_gha
Refactor Cypress GHA
2022-07-22 16:26:22 +02:00
wiz
1244eac03c Merge branch 'master' into knorrium/refactor_cypress_gha 2022-07-22 16:09:45 +02:00
wiz
ce940d7c50 Merge pull request #2149 from knorrium/knorrium/fix_cypress_tv_test
Fix Cypress TV tests
2022-07-22 16:09:38 +02:00
wiz
d0ff377086 Merge pull request #2151 from mempool/simon/navbar-logos-fixes
Navbar logos fix
2022-07-22 16:09:02 +02:00
softsimon
773e43e0cf Navbar logos fix 2022-07-22 14:28:18 +02:00
Felipe Knorr Kuhn
68f63683f1 Remove wrong path setting 2022-07-21 23:10:41 -07:00
Felipe Knorr Kuhn
b5072f823c Fix wrong browser var 2022-07-21 23:04:10 -07:00
Felipe Knorr Kuhn
3d75022a6c Simplify Cypress GHA 2022-07-21 23:02:26 -07:00
Felipe Knorr Kuhn
a030fbedb4 Skip TV tests on signet 2022-07-21 22:32:15 -07:00
Felipe Knorr Kuhn
310bb3cf24 Comment out unused interceptors 2022-07-21 22:22:34 -07:00
Felipe Knorr Kuhn
fa18796109 Update interceptors 2022-07-21 22:07:01 -07:00
Felipe Knorr Kuhn
41bd89521d Fix tests trying to click on the TV button on the navbar 2022-07-21 21:41:33 -07:00
Felipe Knorr Kuhn
29f36d7f9e Add an id to the new tv button 2022-07-21 21:41:03 -07:00
Felipe Knorr Kuhn
2dc875bb33 Merge branch 'master' into nymkappa/bugfix/index-blocks-prices-often 2022-07-21 16:29:24 -07:00
wiz
e7e907d535 Merge pull request #2148 from mempool/simon/enterprise-logo-container
Enterprise logo container
2022-07-22 01:28:11 +02:00
softsimon
a705e44c09 Removing dead code 2022-07-22 01:13:13 +02:00
wiz
72ec4c0d7b Merge branch 'master' into simon/enterprise-logo-container 2022-07-22 01:08:18 +02:00
wiz
7f405ffcdd Merge pull request #2141 from mempool/simon/taproot-button
Always show taproot button
2022-07-22 01:07:40 +02:00
softsimon
dfe50d4355 Enterprise logo container 2022-07-22 01:07:19 +02:00
wiz
181c13025d Merge pull request #2109 from mempool/nymkappa/bugfix/block-audit-code-refactor
Block audit code refactor
2022-07-22 01:07:02 +02:00
wiz
219694db8b Merge pull request #2138 from mempool/nymkappa/bugfix/prediction-lablels
Fix block predition graph x axis labels
2022-07-22 01:06:01 +02:00
wiz
7b3f5c378d Merge pull request #2140 from mempool/simon/correct-gitignore-json-wildcard
Remove gitignore json wildcard
2022-07-22 01:05:37 +02:00
wiz
621789b20c Merge pull request #2147 from mempool/nymkappa/feature/ln-nodes-map-channels
Create world map of clearnet LN nodes and channels
2022-07-22 00:04:58 +02:00
wiz
7bbfc7872b Fix log msg in channels.api.ts
Co-authored-by: softsimon <softsimon@users.noreply.github.com>
2022-07-22 00:04:02 +02:00
wiz
9698339488 Merge branch 'master' into nymkappa/feature/ln-nodes-map-channels 2022-07-21 23:03:18 +02:00
nymkappa
8503fd2fc0 Remove custom environment background 2022-07-21 22:54:19 +02:00
nymkappa
c839abb479 Create world map of clearnet LN nodes and channels 2022-07-21 22:43:12 +02:00
wiz
fd70a51489 Merge pull request #2146 from mempool/simon/subdomain-logo
Subdomain enterprise logo
2022-07-21 22:41:13 +02:00
softsimon
b1b4bdf575 Subdomain enterprise support 2022-07-21 22:02:26 +02:00
wiz
b52a8c58ab Merge pull request #2145 from mempool/ops/route-new-apis-to-services-backend
Route new APIs to services backend
2022-07-21 11:25:24 -05:00
wiz
58b60c1f68 Route new APIs to services backend 2022-07-21 18:07:28 +02:00
Felipe Knorr Kuhn
f4bb927dbd Merge branch 'master' into nymkappa/bugfix/index-blocks-prices-often 2022-07-20 20:59:27 -07:00
Felipe Knorr Kuhn
76b6d2a21b Merge branch 'master' into nymkappa/bugfix/block-audit-code-refactor 2022-07-20 20:59:11 -07:00
Felipe Knorr Kuhn
93aef078cf Merge branch 'master' into nymkappa/bugfix/prediction-lablels 2022-07-20 20:55:47 -07:00
softsimon
a5f1ba92e4 Always show taproot button
refs #2107
2022-07-20 19:30:00 +02:00
softsimon
3e1e47c49a Remove gitignore json wildcard 2022-07-20 18:29:19 +02:00
wiz
c7909a1ca8 Merge pull request #2139 from mempool/nymkappa/feature/ln-nodes-map
Create lightning nodes world heat map (clearnet)
2022-07-20 09:47:27 -05:00
wiz
e0cc58bc6e Fix typo in kappa's PR 2139 2022-07-20 09:39:11 -05:00
wiz
ddb0272d60 Add worldmap.json from apache/echarts
Source: https://github.com/apache/echarts/blob/master/test/data/map/json/world.json
License: Apache
2022-07-20 09:36:15 -05:00
nymkappa
59f84e82b4 Create lightning nodes world heat map (clearnet) 2022-07-20 11:39:51 +02:00
nymkappa
3dc37dc34d Fix block predition graph x axis labels 2022-07-19 08:00:11 +02:00
wiz
88febf6262 Merge pull request #2131 from mempool/ops/reduce-nginx-cache-time-for-homepage
Reduce nginx cache time for production homepage
2022-07-18 17:56:55 -05:00
wiz
29b8660602 Merge pull request #2130 from mempool/simon/disable-pyro-css
Removing randomness in Fireworks scss
2022-07-18 17:49:59 -05:00
wiz
92da3988da Reduce nginx cache time for production homepage 2022-07-18 17:47:33 -05:00
softsimon
8b1fa82c0c Remove random scss calculation 2022-07-19 02:41:03 +04:00
wiz
d2f13eced1 Merge pull request #2110 from mempool/nymkappa/bugfix/block-prediction-chart-no-data
Fix block prediction chart when there is few or no data available
2022-07-17 19:01:19 -05:00
wiz
d373fd1424 Merge branch 'master' into nymkappa/bugfix/block-prediction-chart-no-data 2022-07-17 18:40:33 -05:00
wiz
d5f5fffd7d Merge pull request #2119 from mempool/nymkappa/bugfix/nodes-per-as-css
Fix node per as table css
2022-07-17 18:40:00 -05:00
wiz
cc8c52e848 Merge branch 'master' into nymkappa/bugfix/nodes-per-as-css 2022-07-17 18:30:43 -05:00
wiz
c41bfe755e Merge pull request #2117 from mempool/nymkappa/bugfix/ln-per-network-text
Fix nodes per network chart localization
2022-07-17 18:29:49 -05:00
wiz
8c667a76a7 Merge pull request #2112 from mempool/nymkappa/bugfix/price-updater-incorrect-log
Ignore Kraken historical price without USD
2022-07-17 18:29:16 -05:00
wiz
57e033a32c Merge pull request #2085 from mempool/nymkappa/bugfix/hashrate-no-difficulty
[Hashrate chart] Fix javascript error if difficulty array is empty
2022-07-17 18:28:51 -05:00
wiz
b776b935a0 Merge pull request #2123 from mempool/nymkappa/feature/ln-per-country-chart
Fix error 500 for `Isle of Man` nodes list
2022-07-17 18:27:26 -05:00
wiz
3633d36b28 Merge branch 'master' into nymkappa/feature/ln-per-country-chart 2022-07-17 18:18:14 -05:00
wiz
b4c6c9c86c Merge pull request #2113 from mempool/nymkappa/feature/tv-to-graph
Move TV button to `/graphs/mempool` graph page
2022-07-17 18:17:58 -05:00
nymkappa
681d9db900 Fix error 500 for Isle of Man nodes list 2022-07-18 01:11:20 +02:00
wiz
0137d29cd2 Merge branch 'master' into nymkappa/feature/tv-to-graph 2022-07-17 18:06:28 -05:00
wiz
36412bedd1 Merge pull request #2118 from mempool/nymkappa/feature/ln-per-country-chart
LN nodes per country pie chart
2022-07-17 18:06:07 -05:00
nymkappa
ccdeb108ee Show country flag emoji 2022-07-18 00:55:47 +02:00
nymkappa
f16076b401 Keep county ISO code in lower case in url 2022-07-18 00:06:48 +02:00
nymkappa
ac611a4518 Fix node per as table css 2022-07-17 23:54:17 +02:00
nymkappa
420ff16c2b Make sure "other" is not clickable 2022-07-17 23:48:58 +02:00
nymkappa
b4bcd84a53 Add link to nodes per country list component 2022-07-17 23:48:58 +02:00
nymkappa
63ebace378 Add LN node per country graph 2022-07-17 23:48:57 +02:00
wiz
75f1b52a2a Merge pull request #2120 from mempool/nymkappa/bugfix/fix-as-isp-naming-convention
Fix naming convention "as" => "isp"
2022-07-17 16:47:13 -05:00
nymkappa
d7f0dc4c05 Fix naming convention "as" => "isp" 2022-07-17 23:29:40 +02:00
wiz
09171c749a Merge pull request #2121 from mempool/nymkappa/feature/ln-nodes-per-as-list
Nodes list per ISP
2022-07-17 16:28:34 -05:00
wiz
971f402ced Merge branch 'master' into nymkappa/feature/ln-nodes-per-as-list 2022-07-17 16:09:13 -05:00
wiz
3768c28b01 Merge pull request #2098 from mempool/nymkappa/feature/ln-nodes-per-country
Add nodes per country table page
2022-07-17 16:09:00 -05:00
nymkappa
dbf60dd4d9 Nodes per ISP list component 2022-07-17 22:57:29 +02:00
nymkappa
683190eaa3 Fix nodes per network chart localization 2022-07-17 10:06:08 +02:00
nymkappa
93e93d44f4 Use country iso code in ln nodes per country page url 2022-07-17 09:53:02 +02:00
nymkappa
0902015264 Use correct country name in component title 2022-07-16 23:25:44 +02:00
nymkappa
b11fb44461 Rename "Nodes" to "Lightning nodes" 2022-07-16 23:23:13 +02:00
nymkappa
376484a937 Nodes per country list component 2022-07-16 23:23:13 +02:00
nymkappa
fc5fd244d0 Get nodes per country list with /lightning/nodes/country/:country API 2022-07-16 23:22:53 +02:00
wiz
561d75c694 Merge pull request #2097 from mempool/nymkappa/feature/ln-nodes-per-as
Add nodes per AS chart
2022-07-16 16:19:39 -05:00
nymkappa
82c0987e7b Update naming convention 2022-07-16 23:12:16 +02:00
wiz
c75138ee50 Merge pull request #2114 from mempool/nymkappa/bugfix/graph-download-button-no-wrap
Fix graph titles layout and text
2022-07-16 15:30:12 -05:00
nymkappa
d40545bd52 Apply fix from PR #2114 2022-07-16 21:43:16 +02:00
nymkappa
4093cc0cbf Fix graph title & download button on mobile
Fix wrong graph title on LN channels & capacity chart
2022-07-16 21:37:45 +02:00
nymkappa
0c71e11cda Move TV button to /graphs/mempool graph page 2022-07-16 21:00:32 +02:00
nymkappa
3edd6f23a5 Add capacity per AS 2022-07-16 11:32:48 +02:00
nymkappa
28cf0f71eb Add nodes AS share chart and table component 2022-07-16 10:44:05 +02:00
nymkappa
2fd34cbd91 Get nodes count per AS by calling /lightning/nodes/asShare API 2022-07-16 09:34:45 +02:00
nymkappa
d6158060e7 Ignore Kraken historical price without USD 2022-07-16 09:27:07 +02:00
nymkappa
e0952a4c1d Wait for the price updater to complete before saving blocks prices 2022-07-16 09:22:45 +02:00
nymkappa
2872d2e299 Refactor BlocksSummariesRepository::$saveSummary 2022-07-15 21:48:39 +02:00
wiz
d8a1c0ac1b Merge pull request #2068 from mempool/feature/nymkappa/block-audit-page
Block audit page
2022-07-15 12:02:30 -05:00
transifex-integration[bot]
058d15e67f Translate /frontend/src/locale/messages.xlf in pl
review completed for the source file '/frontend/src/locale/messages.xlf'
on the 'pl' language.
2022-07-15 14:25:36 +00:00
nymkappa
16c6f030db Fix block prediction chart when no or few data is available 2022-07-15 12:01:21 +02:00
nymkappa
1be7c953ea Save current progress on the block audit page 2022-07-14 20:41:50 +02:00
softsimon
c6f33310e5 Merge pull request #2094 from mempool/nymkappa/debug/insert-once-channels-stats-init
Make sure we have initial channel stats to display after fresh run
2022-07-14 18:51:26 +02:00
softsimon
68205ddb9d Merge pull request #2093 from mempool/nymkappa/bugfix/ln-network-stats-layout
Fix LN dashboard layout during indexing
2022-07-14 18:30:42 +02:00
softsimon
dd683094da Merge pull request #2099 from mempool/nymkappa/bugfix/missing-migration
Re-add missing migration that was dropped during merge conflict
2022-07-14 18:00:12 +02:00
nymkappa
681708ffa0 Re-add missing migration that was dropped during merge conflict 2022-07-13 07:56:17 +02:00
wiz
9a29b4adf3 Merge pull request #1817 from mempool/nymkappa/feature/block-fee-usd-chart 2022-07-12 23:35:35 +02:00
wiz
ee058deb74 Merge branch 'master' into nymkappa/feature/block-fee-usd-chart 2022-07-12 23:24:31 +02:00
wiz
2f0c8d94b0 Merge pull request #2095 from mempool/nymkappa/feature/ln-nodes-maxmind
Show maxming data when available is nodes page
2022-07-12 21:40:10 +02:00
nymkappa
6e09a1c96b Show division and ASN number 2022-07-12 21:28:02 +02:00
nymkappa
f2983e28a3 Show city, country and AS name in node page when available 2022-07-12 21:07:38 +02:00
nymkappa
c6fa8c6172 Format mysql query so I can actually update it 2022-07-12 20:18:41 +02:00
nymkappa
1988971290 Make sure we have initial channel stats to display after fresh run 2022-07-12 19:59:37 +02:00
nymkappa
6ef200ae7f Fix LN dashboard layout during indexing 2022-07-12 19:14:43 +02:00
nymkappa
908635b3dd Fix mysql syntax error 2022-07-12 19:10:57 +02:00
wiz
a4946de028 Merge remote-tracking branch 'origin/master' into nymkappa/feature/block-fee-usd-chart 2022-07-12 14:55:47 +02:00
wiz
54931cb23e Merge pull request #2088 from mempool/nymkappa/feature/as_organization
[LN] Add `as_organization` in nodes table
2022-07-12 14:15:19 +02:00
nymkappa
3d2ff7ef62 Store AS organization in geo_names 2022-07-12 12:12:10 +02:00
nymkappa
89b2e11083 [Hashrate chart] Fix javascript error if difficulty array is empty 2022-07-12 09:03:39 +02:00
nymkappa
5ac9b5674e Show "indexing in progress" in fee/reward charts during block indexing 2022-07-12 08:50:07 +02:00
nymkappa
a97675c538 Add daily historical price - show USD in block fee reward charts 2022-07-11 23:16:48 +02:00
nymkappa
40634a0eb8 [Indexing] Link blocks to their closest known price 2022-07-11 22:14:59 +02:00
nymkappa
80b3b91a82 Add USD serie in block fee/reward charts 2022-07-11 22:10:25 +02:00
wiz
47ad5fffc8 Merge pull request #2077 from erikarvstedt/contributors-require-signing 2022-07-11 21:21:13 +02:00
wiz
75bb586f38 Merge pull request #2076 from mempool/simon/backend-routes-refactor 2022-07-11 21:20:43 +02:00
Erik Arvstedt
c743381d33 CONTRIBUTING.md: Ask contributors to sign their commits 2022-07-11 19:42:56 +02:00
softsimon
2253dd570d Refactoring backend routes code 2022-07-11 19:15:28 +02:00
wiz
97046a7dc4 Merge pull request #2069 from mempool/nymkappa/bugfix/update-log-indexer
[Indexer] Set log level accordingly - Remove indexing ETAs
2022-07-11 18:43:42 +02:00
wiz
475bb11991 Merge branch 'master' into nymkappa/bugfix/update-log-indexer 2022-07-11 18:18:20 +02:00
wiz
fd35c8f4ad Merge pull request #2075 from mempool/simon/logger-lightning
Add lightning to logger
2022-07-11 18:18:11 +02:00
wiz
929a4b955c Merge pull request #2074 from mempool/simon/maxmind
Use maxmind to store node locations
2022-07-11 18:18:00 +02:00
softsimon
ca86364c35 Add lightning to logger 2022-07-11 18:02:54 +02:00
wiz
c888d59368 Merge branch 'master' into nymkappa/bugfix/update-log-indexer 2022-07-11 18:01:15 +02:00
wiz
495cd26219 Merge pull request #2070 from erikarvstedt/fix-resources-access
frontend: Always reference `resources` relative to root
2022-07-11 18:01:05 +02:00
softsimon
519494668b Use maxmind to store node locations 2022-07-11 17:52:38 +02:00
wiz
73b2be0a97 Merge branch 'master' into fix-resources-access 2022-07-11 17:12:24 +02:00
wiz
9fce787105 Merge pull request #2073 from mempool/simon/populate-historical-node-data
Populate historical node data
2022-07-11 17:08:28 +02:00
wiz
8c5460c319 Merge pull request #2071 from mempool/wiz/add-maxmind-geoipdb
Add maxmind geoip-db update utility to prod installer
2022-07-11 16:42:24 +02:00
wiz
bd796ae8cc Merge branch 'master' into simon/populate-historical-node-data 2022-07-11 16:42:04 +02:00
wiz
97c05facdb Merge pull request #2072 from erikarvstedt/nxinx-gixy-fixes
nginx: Fix errors found by gixy (nginx conf static analyzer)
2022-07-11 16:41:56 +02:00
wiz
46bed0be29 Merge branch 'master' into simon/populate-historical-node-data 2022-07-11 15:58:58 +02:00
Erik Arvstedt
81bc449043 nginx: Fix gixy test host_spoofing
This patch was generated by replacing:
`proxy_set_header Host $http_host` ->
`proxy_set_header Host $host`

Script:
find . -type f -exec sed -i 's|proxy_set_header Host \$http_host|proxy_set_header Host \$host|g' {} \;

Fixes test error:
```
>> Problem: [host_spoofing] The proxied Host header may be spoofed.
Description: In most cases "$host" variable are more appropriate, just use it.
Additional info: https://github.com/yandex/gixy/blob/master/docs/en/plugins/hostspoofing.md
```

`proxy_set_header Host $host` is indeed the recommended default proxy header setting.
2022-07-11 15:32:37 +02:00
Erik Arvstedt
eec82e1bf9 nginx: Fix gixy test http_splitting
Fixes test error:
```
>> Problem: [http_splitting] Possible HTTP-Splitting vulnerability.
Description: Using variables that can contain "\n" or "\r" may lead to http injection.
```

Summary: `$uri` should never be used in `return` statements.
See: https://github.com/yandex/gixy/blob/master/docs/en/plugins/httpsplitting.md

In this case, `$uri` always equals `/`, so just replace it.
2022-07-11 15:25:42 +02:00
softsimon
1c86273059 Run daily stats at midnight and backfill first launch 2022-07-11 14:45:25 +02:00
wiz
7320fadec9 Add maxmind geoip-db update utility to prod installer 2022-07-11 14:32:38 +02:00
Erik Arvstedt
355e89ce55 frontend URLs: ./resources -> /resources
This patch was created by:
find ./frontend -type f -exec sed -i 's|\./resources|/resources|g' {} \;
2022-07-11 13:33:25 +02:00
Erik Arvstedt
90b9c5fe8a frontend URLs: *../resources -> /resources 2022-07-11 13:32:23 +02:00
wiz
a458cf8ee3 Merge branch 'master' into nymkappa/bugfix/update-log-indexer 2022-07-11 11:31:39 +02:00
wiz
4b3cc7396c Merge pull request #2067 from mempool/nymkappa/feature/block-api-dynamic-caching
Set /block API cache duration according to block age
2022-07-11 11:30:45 +02:00
wiz
e86c5987e3 Merge branch 'master' into nymkappa/feature/block-api-dynamic-caching 2022-07-11 11:19:03 +02:00
wiz
65ce49817c Merge pull request #2066 from mempool/nymkappa/bugfix/graph-buttons
Fix graphs button layout
2022-07-11 11:18:01 +02:00
nymkappa
38ac38849e [Indexer] Set log level accordingly - Remove indexing ETAs 2022-07-11 11:07:41 +02:00
wiz
960c31a3c7 Merge branch 'master' into nymkappa/bugfix/graph-buttons 2022-07-11 10:57:33 +02:00
wiz
38fa8de01f Merge pull request #2065 from mempool/nymkappa/bugfix/liquid-block-api
Liquid always uses esplora (regression of #2039)
2022-07-11 10:56:42 +02:00
nymkappa
a4641b8480 Set /block API cache duration according to block age 2022-07-11 09:53:32 +02:00
nymkappa
f2e703e928 Fix graphs button layout 2022-07-11 09:36:42 +02:00
nymkappa
0093eab269 Liquid always uses esplora (regression of #2039) 2022-07-11 08:41:28 +02:00
wiz
b8b50b552e Merge pull request #2026 from mempool/dependabot/npm_and_yarn/frontend/tinyify-3.1.0
Bump tinyify from 3.0.0 to 3.1.0 in /frontend
2022-07-10 19:35:00 +02:00
softsimon
665d85204b Backfill node_stats 2022-07-10 19:28:21 +02:00
wiz
291277f299 Merge branch 'master' into dependabot/npm_and_yarn/frontend/tinyify-3.1.0 2022-07-10 19:24:39 +02:00
wiz
8c94ef4a03 Merge pull request #1981 from mempool/nymkappa/feature/ln-chart-in-graph-component
Add LN charts into `/graphs` and add timespan selection
2022-07-10 19:17:42 +02:00
nymkappa
ed3aa7f516 Add Lightning charts in /graph 2022-07-10 19:03:50 +02:00
wiz
37f731d21c Merge pull request #1980 from mempool/nymkappa/feature/channels-stats-widget
Index LN channels stats and show them in dashboard widget
2022-07-10 17:52:31 +02:00
softsimon
4ccaafcd63 Removing empty column 2022-07-10 17:40:46 +02:00
wiz
c8e090149a Fix kappa's accidental search and replace 2022-07-10 17:28:47 +02:00
wiz
9c6a28d9b0 Fix number of arguments in SQL query 2022-07-10 17:24:43 +02:00
nymkappa
9000b6b18e Index daily channel stats and show in dashboard widget 2022-07-10 17:09:01 +02:00
wiz
4009a066e0 Merge pull request #1978 from mempool/nymkappa/feature/ln-nodes-networks
Add nodes per network chart component
2022-07-10 16:42:09 +02:00
nymkappa
d2135a374a Add nodes per network chart component 2022-07-10 16:22:53 +02:00
wiz
e8a3b104f8 Merge pull request #2059 from mempool/nymkappa/bugfix/diff-rounding
Don't round signet difficulty in table and chart
2022-07-10 16:17:00 +02:00
wiz
5437beedef Merge branch 'master' into nymkappa/bugfix/diff-rounding 2022-07-10 15:08:50 +02:00
wiz
b7709ac3d0 Merge pull request #1976 from mempool/simon/lightning-pr
Lightning
2022-07-10 15:08:37 +02:00
wiz
a638369a57 Bump version to v2.5.0-dev 2022-07-10 14:53:57 +02:00
wiz
22bd8c4bf8 Fix HTTP 501 -> HTTP 400 suggestions in PR review 2022-07-10 14:51:09 +02:00
nymkappa
eb71276948 Don't round signet difficulty in table and chart 2022-07-10 14:32:15 +02:00
softsimon
18030ba33e Updating backend package-lock 2022-07-10 14:23:12 +02:00
softsimon
2129146838 Revert "Updating package lock"
This reverts commit bd89bf885d.
2022-07-10 14:21:33 +02:00
softsimon
b6a113f05c Fixing titles. 2022-07-10 14:07:53 +02:00
softsimon
bd89bf885d Updating package lock 2022-07-10 14:03:49 +02:00
wiz
7d3c105b29 Merge branch 'master' into simon/lightning-pr 2022-07-10 14:00:02 +02:00
wiz
a99b52a735 Merge pull request #2057 from mempool/simon/transifex-pull-0710
Pull from transifex
2022-07-10 13:57:58 +02:00
wiz
e58b71fd4f Merge pull request #2052 from mempool/nymkappa/bugfix/blocks-list-pagination-error
Fix pagination issue in blocks list
2022-07-10 13:56:38 +02:00
wiz
59d10fd3c6 Merge pull request #2051 from mempool/nymkappa/bugfix/diff-adj-table-raw-db-data
Fix diff adj table using raw db value
2022-07-10 13:14:56 +02:00
wiz
30e8b134bc Merge branch 'master' into nymkappa/bugfix/diff-adj-table-raw-db-data 2022-07-10 12:56:30 +02:00
nymkappa
cdf0fe0335 Fix first diff adjustmeent if INDEXING_BLOCK_AMOUNT is not -1 2022-07-10 12:39:22 +02:00
nymkappa
f4667c0892 Use raw diff adjustment for diff adj table widget 2022-07-10 12:39:21 +02:00
wiz
ac257b4165 Merge branch 'master' into nymkappa/bugfix/blocks-list-pagination-error 2022-07-10 12:37:51 +02:00
wiz
75ab2bc920 Merge pull request #2054 from mempool/nymkappa/bugfix/dont-insert-price-no-usd
Ignore prices without USD exchange rate
2022-07-10 12:37:43 +02:00
wiz
307c30e33b Merge pull request #2056 from mempool/nymkappa/feature/config-automatic-block-reindex
Disable automatic block re-indexing by default
2022-07-10 12:36:43 +02:00
softsimon
c7835b1326 Pull from transifex 2022-07-10 12:29:19 +02:00
wiz
7e389c8863 Merge branch 'master' into nymkappa/bugfix/dont-insert-price-no-usd 2022-07-10 12:17:08 +02:00
wiz
d697c0c45e Merge branch 'master' into nymkappa/feature/config-automatic-block-reindex 2022-07-10 12:10:38 +02:00
wiz
4fa4088694 Merge pull request #2033 from mempool/nymkappa/bugfix/cleanup-hashrate-indexing
Fix hashrate indexing, log difficulty adjustment progress
2022-07-10 12:06:34 +02:00
nymkappa
07cb4a49bc Index weekly mining pool hashrate only if there are blocks mined 2022-07-09 22:34:18 +02:00
nymkappa
067ee168dd Use oldest consecutive block timestamp as hashrate indexing limit 2022-07-09 22:11:27 +02:00
nymkappa
d8a90cce47 Use raw db hashrate instead of avg for indexing 2022-07-09 22:11:27 +02:00
nymkappa
e303a4c374 Removed hardcoded genesis timestamp
Fix duplicated genesis hashrate attempt
Add log during difficulty adjustment indexing
2022-07-09 22:11:26 +02:00
wiz
c75f485e54 Merge pull request #1999 from mempool/nymkappa/bugfix/schedule-indexing-updates
Run the mining indexer every hour to index missing/updated data
2022-07-09 20:07:21 +02:00
nymkappa
f3f0c688d8 Disable automatic block re-indexing by default 2022-07-09 19:04:35 +02:00
nymkappa
030020ea9e Run the mining indexer every hour to index missing/updated data 2022-07-09 18:25:16 +02:00
Felipe Knorr Kuhn
654e7589a6 Merge branch 'knorrium/backend_unit_tests' of github.com:knorrium/mempool into knorrium/backend_unit_tests 2022-07-09 07:38:52 -07:00
Felipe Knorr Kuhn
de8d3d7e3e Add missing dependencies 2022-07-09 07:38:27 -07:00
Felipe Knorr Kuhn
d2bae2fa8b Merge branch 'master' into knorrium/backend_unit_tests 2022-07-09 16:36:10 +02:00
Felipe Knorr Kuhn
057b1c7569 Merge branch 'master' into dependabot/npm_and_yarn/frontend/tinyify-3.1.0 2022-07-09 16:34:28 +02:00
nymkappa
682682c74a Ignore prices without USD exchange rate 2022-07-09 16:13:01 +02:00
nymkappa
b5daf205a0 Fix pagination issue in blocks list 2022-07-09 16:02:43 +02:00
wiz
1037fbe52b Merge pull request #2041 from Emzy/ops/freebsd-nginx-conf
Install nginx config also for freebsd on prod install
2022-07-09 14:33:24 +02:00
wiz
051d151fb7 Merge pull request #2002 from mempool/nymkappa/feature/automatic-block-reindexing
Automatic block re-indexing upon pools.json update
2022-07-09 13:28:44 +02:00
Felipe Knorr Kuhn
9b6bbaf51c Merge branch 'master' into knorrium/backend_unit_tests 2022-07-09 00:08:49 +02:00
Felipe Knorr Kuhn
735a069b6d Merge branch 'master' into dependabot/npm_and_yarn/frontend/tinyify-3.1.0 2022-07-09 00:04:51 +02:00
Felipe Knorr Kuhn
7ba7440bb6 Merge branch 'master' into nymkappa/feature/automatic-block-reindexing 2022-07-09 00:04:35 +02:00
wiz
dafbd5cc43 Merge branch 'master' into simon/lightning-pr 2022-07-08 18:56:33 +02:00
wiz
81f20e53ea Merge pull request #2043 from mempool/translations_frontend-src-locale-messages-xlf--master_nb
Translate '/frontend/src/locale/messages.xlf' in 'nb'
2022-07-08 18:56:09 +02:00
wiz
05a2c05a9e Merge pull request #2044 from mempool/translations_frontend-src-locale-messages-xlf--master_fr
Translate '/frontend/src/locale/messages.xlf' in 'fr'
2022-07-08 18:55:44 +02:00
softsimon
c6d56f06b2 Import echarts for the lightning module more efficiently 2022-07-08 18:55:35 +02:00
softsimon
73c4a934ce Replacing ln-service library and wait for graph sync. 2022-07-08 18:55:35 +02:00
softsimon
1f2254681a Search result fix when Lightning not enabled 2022-07-08 18:55:26 +02:00
softsimon
850060cc07 Search result click fix. 2022-07-08 18:55:26 +02:00
softsimon
89c4023ddf Fix for search box tests 2022-07-08 18:55:26 +02:00
softsimon
17a6b7fefd Restoring the TV button 2022-07-08 18:55:26 +02:00
softsimon
71b304cafd Renaming config LND_NODE_AUTH to LND 2022-07-08 18:55:26 +02:00
nymkappa
a238420d7f Merge Lightning backend into Mempool backend 2022-07-08 18:55:26 +02:00
softsimon
faafa6db3b Backfill node and capacity 2022-07-08 18:55:26 +02:00
softsimon
1f6008f269 Store channel closing date 2022-07-08 18:55:25 +02:00
softsimon
d32579dfb5 Adding missing *.json files 2022-07-08 18:55:25 +02:00
softsimon
cea7ce140f Making socket configurable 2022-07-08 18:55:25 +02:00
softsimon
3ecce35b11 Minor tls spelling fix 2022-07-08 18:55:25 +02:00
softsimon
a8fd04e2f0 Adding missing migration index key 2022-07-08 18:55:25 +02:00
softsimon
f2e42b17a7 Fix TLS misspelling 2022-07-08 18:55:25 +02:00
softsimon
f46543b264 Node graphs 2022-07-08 18:55:25 +02:00
softsimon
d8a39f2e49 Timestamp component 2022-07-08 18:55:24 +02:00
softsimon
24631116c4 Dashboard stats graph 2022-07-08 18:55:24 +02:00
softsimon
3152effba5 Sats component rounding 2022-07-08 18:55:24 +02:00
softsimon
4d83478e7d Adding TLV to channel details 2022-07-08 18:55:24 +02:00
softsimon
da9834d272 Label channel closes 2022-07-08 18:55:24 +02:00
softsimon
4bb23cf0c8 Closed channels forensics 2022-07-08 18:55:24 +02:00
softsimon
f0ad38dec6 Search fix. Channel update. 2022-07-08 18:55:24 +02:00
softsimon
b87308e14f Merge conflict fix. 2022-07-08 18:55:24 +02:00
softsimon
473cb55dc4 Channel pagination 2022-07-08 18:55:23 +02:00
softsimon
11a7babbc4 Improving channels api with node data 2022-07-08 18:55:23 +02:00
softsimon
9ebc8813e3 Node and Channel pages improvements 2022-07-08 18:55:23 +02:00
softsimon
ac10aafc07 More node info 2022-07-08 18:55:23 +02:00
softsimon
8604869e5e Search bar refactored with Nodes and Channels 2022-07-08 18:55:23 +02:00
softsimon
1ed4c93b94 Search API 2022-07-08 18:55:23 +02:00
softsimon
caadae3f98 Updated migration script with latest database model. 2022-07-08 18:55:23 +02:00
softsimon
67eab93129 Link channels from Transaction page. 2022-07-08 18:55:23 +02:00
softsimon
31d280f729 Adding Lightning wrapper component 2022-07-08 18:55:22 +02:00
softsimon
d23e5d0e87 Node qr code 2022-07-08 18:55:22 +02:00
softsimon
774215a073 Socket selector and copy 2022-07-08 18:55:22 +02:00
softsimon
07821769cd Node stats updates 2022-07-08 18:55:22 +02:00
softsimon
b0b73e6c70 Adding channel id in addition to short id 2022-07-08 18:55:22 +02:00
softsimon
7e1c2f4f40 Find inactive channels with dead nodes. 2022-07-08 18:55:22 +02:00
softsimon
65c731e1ad Channel status 2022-07-08 18:55:22 +02:00
softsimon
795bb6a7a6 Channel component 2022-07-08 18:55:22 +02:00
softsimon
f5325b3a6d Node and channel API 2022-07-08 18:55:21 +02:00
softsimon
8d622e3606 Store and display stats and node top lists 2022-07-08 18:55:21 +02:00
softsimon
3e6af8e87b Store nodes and channels 2022-07-08 18:55:21 +02:00
softsimon
948f905a66 Store network stats 2022-07-08 18:55:21 +02:00
softsimon
93b398a54f Lightning explorer base structure 2022-07-08 18:55:21 +02:00
wiz
44b1daeed2 Merge pull request #2046 from knorrium/knorrium/fix-package-lock
Fix missing dependencies
2022-07-08 18:55:17 +02:00
Felipe Knorr Kuhn
bed266abac Fix missing dependencies 2022-07-08 09:40:14 -07:00
wiz
8487548271 Merge pull request #2045 from mempool/wiz/update-nodejs-v16.16.0
Update to Node.js v16.16.0, use shared zlib for prod
2022-07-08 18:33:06 +02:00
wiz
c5e8a83ebb Update to Node.js v16.16.0, use shared zlib for prod 2022-07-08 17:22:56 +02:00
transifex-integration[bot]
b6f81bc83a Translate /frontend/src/locale/messages.xlf in nb
review completed for the source file '/frontend/src/locale/messages.xlf'
on the 'nb' language.
2022-07-08 15:07:38 +00:00
transifex-integration[bot]
294c278c42 Translate /frontend/src/locale/messages.xlf in fr
review completed for the source file '/frontend/src/locale/messages.xlf'
on the 'fr' language.
2022-07-08 15:07:38 +00:00
wiz
ff88a65936 Merge pull request #2036 from mempool/simon/amount-string-merge
Renaming value -> amount
2022-07-08 17:07:23 +02:00
wiz
76c7508224 Merge pull request #2037 from knorrium/knorrium/node_matrix
Run the CI action on 16 and 18
2022-07-08 17:06:51 +02:00
dependabot[bot]
49723c4d1b Bump tinyify from 3.0.0 to 3.1.0 in /frontend
Bumps [tinyify](https://github.com/browserify/tinyify) from 3.0.0 to 3.1.0.
- [Release notes](https://github.com/browserify/tinyify/releases)
- [Changelog](https://github.com/browserify/tinyify/blob/default/CHANGELOG.md)
- [Commits](https://github.com/browserify/tinyify/compare/v3.0.0...v3.1.0)

---
updated-dependencies:
- dependency-name: tinyify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-08 15:05:02 +00:00
wiz
3c02131133 Merge pull request #2042 from mempool/simon/revert-angular-14-upgrade
Revert: Angular 14 upgrade
2022-07-08 17:03:49 +02:00
wiz
c89fd8c39f Merge pull request #2040 from Emzy/ops/disable-dialog
Remove the dialog for now on prod install
2022-07-08 16:55:40 +02:00
wiz
69623d71b2 Merge pull request #2039 from mempool/nymkappa/bugfix/use-core-for-block-list
Use bitcoin core to fetch blocks
2022-07-08 16:50:34 +02:00
softsimon
2d6f4d3bdb Revert: Angular 14 upgrade 2022-07-08 16:45:24 +02:00
nymkappa
97ff1e37aa Use bitcoin core instead of esplore for fetch blocks on bitcoin networks 2022-07-08 16:34:00 +02:00
Stephan Oeste
d0381e7850 Install nginx config also for freebsd on prod install 2022-07-08 15:59:39 +02:00
wiz
c4638f2ac5 Merge pull request #2035 from Emzy/ops/build-mempool
Add mempool build as final step on prod install
2022-07-08 15:34:50 +02:00
wiz
83c383b1ec Print onions on separate lines after prod install 2022-07-08 15:34:10 +02:00
Stephan Oeste
4f22864080 Add mempool build as final step on prod install 2022-07-08 15:29:59 +02:00
Stephan Oeste
92780daa78 Remove the dialog for now on prod install 2022-07-08 15:26:13 +02:00
Felipe Knorr Kuhn
a5e4b09e64 Set fail fast to false on the frontend jobs 2022-07-08 05:13:26 -07:00
Felipe Knorr Kuhn
1501dd23ab Continue running CI jobs if something fails 2022-07-08 04:49:51 -07:00
Felipe Knorr Kuhn
15ab134fa4 Run the CI action on 16 and 18 2022-07-08 04:44:19 -07:00
softsimon
05f0ba72e2 Renaming value -> amount 2022-07-08 13:31:10 +02:00
Felipe Knorr Kuhn
15fcbf18bf Merge branch 'master' into knorrium/backend_unit_tests 2022-07-08 13:14:33 +02:00
wiz
c9c5e8008c Merge pull request #2030 from mempool/simon/angular-14-upgrade
Angular 14 Upgrade
2022-07-08 12:10:10 +02:00
wiz
bdd3af6b6a Merge pull request #2032 from Emzy/ops/fix-elements-service
Fix service name to elements and add elements testnet for prod install
2022-07-08 11:57:36 +02:00
Stephan Oeste
6582c8b36f Fix service name to elements and add elements testnet for prod install 2022-07-08 11:54:26 +02:00
wiz
be838ec313 Merge pull request #2031 from Emzy/ops/fix-echo
Quote echo output in prod install
2022-07-08 11:33:03 +02:00
Stephan Oeste
92eef3a6c1 Quote echo output in prod install 2022-07-08 11:05:45 +02:00
softsimon
63939981c1 Angular 14 Upgrade 2022-07-08 10:44:07 +02:00
Felipe Knorr Kuhn
fc629d7109 Merge branch 'master' into knorrium/backend_unit_tests 2022-07-07 15:57:14 -07:00
wiz
c9f788e3a4 Merge pull request #2012 from Emzy/ops/nginx-linux-patch
Installing Linux nginx config in prod install
2022-07-07 22:52:16 +02:00
wiz
0a866b468a Merge branch 'master' into nymkappa/feature/automatic-block-reindexing 2022-07-07 22:41:26 +02:00
wiz
8f0f755014 Merge pull request #2020 from mempool/nymkappa/bugfix/duplicate-genesis-hashrate-indexing
[Hashrate indexing] - Signet started in 2020 and not in 2009
2022-07-07 22:41:07 +02:00
Felipe Knorr Kuhn
6a802d7e80 Merge branch 'master' into knorrium/backend_unit_tests 2022-07-07 13:08:50 -07:00
nymkappa
5943b88ffe [Hashrate indexing] - Signet started in 2020 and not in 2009 2022-07-07 21:55:28 +02:00
Felipe Knorr Kuhn
7bc3ece5b5 Change @bable/core to the devDependencies 2022-07-07 12:54:47 -07:00
wiz
c5dfe92e60 Merge pull request #2017 from mempool/translations_frontend-src-locale-messages-xlf--master_nb
Translate '/frontend/src/locale/messages.xlf' in 'nb'
2022-07-07 21:52:13 +02:00
wiz
753cf3cbac Merge pull request #2018 from mempool/translations_frontend-src-locale-messages-xlf--master_fr
Translate '/frontend/src/locale/messages.xlf' in 'fr'
2022-07-07 21:52:04 +02:00
Felipe Knorr Kuhn
1b9100a7f7 Reduce the coverage threshold to 1% 2022-07-07 12:49:21 -07:00
Felipe Knorr Kuhn
af27d68add Fix errors while building in prod mode 2022-07-07 12:47:31 -07:00
Felipe Knorr Kuhn
07610c7ed0 Fix backend dependencies again 2022-07-07 12:38:50 -07:00
Felipe Knorr Kuhn
6bb1b8a64c Fix duplicate key in tsconfig 2022-07-07 12:38:16 -07:00
Felipe Knorr Kuhn
90dd78c2ec Update package-lock 2022-07-07 12:35:47 -07:00
Felipe Knorr Kuhn
dc258da6c0 Add missing tsconfig file 2022-07-07 12:33:04 -07:00
Felipe Knorr Kuhn
2c222c36d2 Fix package-lock merge conflicts 2022-07-07 12:30:42 -07:00
Felipe Knorr Kuhn
3c92919359 Enable unit testing on the backend on the CI 2022-07-07 12:23:21 -07:00
Felipe Knorr Kuhn
f36fa62569 Add unit tests for the backend: Config 2022-07-07 12:22:08 -07:00
Felipe Knorr Kuhn
451e36e288 Update gitignore 2022-07-07 12:21:41 -07:00
Felipe Knorr Kuhn
352f0817d9 Add Jest to support backend unit tests 2022-07-07 12:21:30 -07:00
wiz
4a64984d7f Merge pull request #2014 from mempool/wiz/cleanup-package-lock
Remove unused deps from backend/package-lock.json
2022-07-07 21:13:50 +02:00
Stephan Oeste
eeb84e5d42 Installing Linux nginx config in prod install 2022-07-07 21:09:00 +02:00
transifex-integration[bot]
50cd8c80d8 Translate /frontend/src/locale/messages.xlf in fr
review completed for the source file '/frontend/src/locale/messages.xlf'
on the 'fr' language.
2022-07-07 18:55:27 +00:00
wiz
567d4aebbc Merge branch 'master' into nymkappa/feature/automatic-block-reindexing 2022-07-07 20:55:08 +02:00
Felipe Knorr Kuhn
b53cc4c37c Merge branch 'master' into wiz/cleanup-package-lock 2022-07-07 11:41:59 -07:00
wiz
d46e1abd07 Merge pull request #2011 from mempool/wiz/fix-npm-install-no-optional
Fix npm install commands in Dockerfiles and ops scripts
2022-07-07 20:37:34 +02:00
wiz
0b0c0b458f Merge branch 'master' into wiz/fix-npm-install-no-optional 2022-07-07 20:36:58 +02:00
wiz
997b5a1c9d Merge pull request #2015 from mempool/simon/adding-dropdown-component-to-project
Moving ngx-bootrap-multiselect to the project
2022-07-07 20:36:35 +02:00
transifex-integration[bot]
4345661a0b Translate /frontend/src/locale/messages.xlf in nb
review completed for the source file '/frontend/src/locale/messages.xlf'
on the 'nb' language.
2022-07-07 18:30:31 +00:00
softsimon
5867c79a1f Moving ngx-bootrap-multiselect to the project 2022-07-07 20:14:58 +02:00
wiz
41f0619572 Merge pull request #2013 from Emzy/ops/set-user-ulimit
Set ulimit highter for all users in prod install
2022-07-07 19:56:38 +02:00
wiz
96b4ea6b50 Merge pull request #1998 from Emzy/ops/fix-linux-crontab
Make user crontab reliable in prod install script
2022-07-07 19:56:06 +02:00
wiz
8dda51a92a Remove unused deps from backend/package-lock.json 2022-07-07 19:33:43 +02:00
Stephan Oeste
b4bb54212c Set ulimit highter for all users in prod install 2022-07-07 19:22:47 +02:00
wiz
d57193c269 Fix npm install commands in Dockerfiles and ops scripts 2022-07-07 18:53:17 +02:00
wiz
4bc03c2d60 Merge branch 'master' into nymkappa/feature/automatic-block-reindexing 2022-07-07 18:21:04 +02:00
Stephan Oeste
bf969ec8f7 Make user crontab reliable in prod install script 2022-07-07 17:13:09 +02:00
wiz
6ead907e08 Merge pull request #1953 from mononaut/breathe-effect-framerate
limit pulsing blocks animation frame rate to 30FPS
2022-07-07 16:56:18 +02:00
wiz
243168a450 Merge branch 'master' into breathe-effect-framerate 2022-07-07 16:43:03 +02:00
wiz
06d2cf1b88 Merge pull request #2010 from mempool/simon/transifex-pull-0707
Pulled from Transifex
2022-07-07 16:42:24 +02:00
softsimon
d622162f33 Pulled from Transifex 2022-07-07 16:40:18 +02:00
wiz
12807583c2 Merge pull request #2003 from mempool/ops/fix-npm-install-prod
Add `--prod` to `npm install` in ops scripts
2022-07-07 16:36:11 +02:00
wiz
47f3d539c3 Merge pull request #2004 from mempool/nymkappa/bugfix/jumping-pagination
Set pagination font to monospace
2022-07-07 16:35:29 +02:00
wiz
993cd64126 Merge pull request #2008 from mempool/wiz/fix-mononaut-tooltip-width
Increase width of mononaut transaction tooltip
2022-07-07 16:34:56 +02:00
softsimon
a0e32ab0bd Auto resize block overview tooltip 2022-07-07 16:25:26 +02:00
wiz
409763b885 Merge pull request #2009 from mempool/simon/bisq-graphs-resize
Auto resize graphs on window resize
2022-07-07 15:58:55 +02:00
softsimon
e06819fc6f Auto resize graphs on window resize
fixes #1607
2022-07-07 15:46:13 +02:00
wiz
812783f2cd Increase width of mononaut transaction tooltip
Fixes #2007
2022-07-07 15:43:03 +02:00
Felipe Knorr Kuhn
849373a6d3 Merge branch 'master' into breathe-effect-framerate 2022-07-07 06:36:17 -07:00
wiz
0417d3b70d Merge pull request #2006 from mempool/simon/block-predictions-i18n-string
Merging the 3 "block predictions" i18n string into 1
2022-07-07 15:06:40 +02:00
wiz
a55eb653f9 Merge branch 'master' into simon/block-predictions-i18n-string 2022-07-07 14:53:08 +02:00
softsimon
6b05ed764e Renaming Predictions to Prediction 2022-07-07 14:52:26 +02:00
wiz
766eeb2a6f Merge pull request #2001 from Emzy/ops/fix-bitcoin-startup
Fix startup scripts in prod install
2022-07-07 14:39:02 +02:00
softsimon
753e6bd956 Merging the 3 block predictions title into 1 i18n string 2022-07-07 14:33:33 +02:00
wiz
5e6295d79a Merge pull request #2005 from mempool/simon/transifex-0707
Extracting i18n strings
2022-07-07 14:22:32 +02:00
wiz
3b611275d2 Merge pull request #2000 from mempool/knorrium/prettier
Add editorconfig and prettier integration with eslint
2022-07-07 14:21:31 +02:00
wiz
cb4dac3506 Merge remote-tracking branch 'origin/master' into knorrium/prettier 2022-07-07 14:13:06 +02:00
wiz
9a7dc3fa49 Merge pull request #1986 from knorrium/mining_e2e_tests_v2
Mining e2e tests
2022-07-07 14:10:12 +02:00
softsimon
e1c833872e Extracting i18n strings 2022-07-07 14:03:18 +02:00
nymkappa
5de559f5ad Set pagination font to monospace 2022-07-07 14:00:54 +02:00
wiz
bc068a0d9a Add --prod to npm install in ops scripts 2022-07-07 13:59:51 +02:00
wiz
13ccc96e03 Merge branch 'master' into mining_e2e_tests_v2 2022-07-07 13:50:23 +02:00
wiz
bc56878039 Merge pull request #1997 from mempool/knorrium/gha_triggers
Update CI to trigger on PRs and exclude from ops activities
2022-07-07 13:50:10 +02:00
wiz
8cb2149fbd Merge pull request #1996 from mempool/knorrium/clean_be_deps
Remove TSLint from the backend
2022-07-07 13:49:40 +02:00
wiz
7d7c331238 Merge pull request #1994 from mempool/update_cypress
Update Cypress related dependencies
2022-07-07 13:49:07 +02:00
Felipe Knorr Kuhn
a8d58d14ff Merge branch 'master' into mining_e2e_tests_v2 2022-07-07 04:47:31 -07:00
nymkappa
4723a9d41b Re-index related blocks when mining pool.json changes 2022-07-07 13:41:09 +02:00
Stephan Oeste
4ee9a42f3f Fix startup scripts in prod install 2022-07-07 12:19:51 +02:00
Felipe Knorr Kuhn
aae2dec16d Add editorconfig and prettier integration with eslint 2022-07-07 03:02:02 -07:00
Felipe Knorr Kuhn
43e0fe655e Update CI to trigger on PRs and exclude from ops activities 2022-07-06 16:29:38 -07:00
Felipe Knorr Kuhn
958d77ed6c Merge branch 'master' into knorrium/clean_be_deps 2022-07-06 15:11:18 -07:00
wiz
ad32ba8a98 Merge pull request #1991 from mempool/fix_electrum_api
Fix electrum API encoding
2022-07-07 00:10:32 +02:00
Felipe Knorr Kuhn
d7847c7630 Merge branch 'master' into mining_e2e_tests_v2 2022-07-06 15:04:08 -07:00
Felipe Knorr Kuhn
284e6e5720 Merge branch 'master' into fix_electrum_api 2022-07-06 14:58:13 -07:00
Felipe Knorr Kuhn
4223bb2047 Merge branch 'master' into knorrium/clean_be_deps 2022-07-06 14:57:59 -07:00
wiz
0fa18be43e Merge pull request #1995 from erikarvstedt/improve-ci
Minor CI improvements
2022-07-06 23:57:25 +02:00
Felipe Knorr Kuhn
43f2faa077 Merge branch 'master' into improve-ci 2022-07-06 14:47:45 -07:00
wiz
f9dfbf94ef Merge branch 'master' into breathe-effect-framerate 2022-07-06 23:45:29 +02:00
wiz
3424bb9d6a Merge pull request #1952 from antonilol/qrcodes
set error correction level to low for qr codes
2022-07-06 23:45:02 +02:00
Erik Arvstedt
6288bcde51 CI: Add --no-optional flag to prod flavor
This ensures that the packages build successfully even when optional
deps are unavailable.
`--no-optional` currently has no effect on the backend, because
it has no optional packages.
2022-07-06 23:43:09 +02:00
Erik Arvstedt
80476a2b61 CI: Use npm ci instead of npm install
`npm ci` is recommended instead of `npm install` for automated builds.
Its main advantage is the `package-lock.json` consistency check: The
command fails if the lock file doesn't match `package.json`.
2022-07-06 23:38:06 +02:00
Erik Arvstedt
78ee671051 CI/frontend: Add missing install flag for prod flavor
Also restrict linting, which requires dev packages, to the `dev`
flavor.
This makes the frontend build setup indentical to the backend.
2022-07-06 23:38:05 +02:00
Erik Arvstedt
386a2de117 contributors: Add erikarvstedt 2022-07-06 23:38:04 +02:00
Felipe Knorr Kuhn
70b2731b82 Remove TSLint from the backend 2022-07-06 14:36:50 -07:00
Felipe Knorr Kuhn
ae3f8b8bd5 Merge branch 'master' into update_cypress 2022-07-06 14:33:17 -07:00
Felipe Knorr Kuhn
2d888d7c13 Merge branch 'master' into fix_electrum_api 2022-07-06 14:33:02 -07:00
wiz
98db8b1b25 Merge branch 'master' into qrcodes 2022-07-06 23:31:01 +02:00
wiz
f710ffb7d0 Merge pull request #1954 from mempool/simon/block-error-code
Block error status code missing fix
2022-07-06 23:30:55 +02:00
wiz
b702782c27 Merge pull request #1993 from mempool/clean_fe_deps
Remove frontend dependencies and old tests
2022-07-06 23:30:41 +02:00
wiz
5dc7fe6a72 Merge pull request #1949 from mempool/feature/nymkappa/block-audit
Save block predictions results in db and show results in a chart
2022-07-06 23:29:42 +02:00
Felipe Knorr Kuhn
39e8f75e07 Merge branch 'clean_fe_deps' of github.com:mempool/mempool into clean_fe_deps 2022-07-06 13:50:54 -07:00
Felipe Knorr Kuhn
f4e0b1125c Disable Scarf dependency from ngx-infinite-scroll 2022-07-06 13:50:02 -07:00
Felipe Knorr Kuhn
663bd118a5 Merge branch 'master' into fix_electrum_api 2022-07-06 13:36:36 -07:00
Felipe Knorr Kuhn
2a8e2d2d25 Merge branch 'master' into clean_fe_deps 2022-07-06 13:36:11 -07:00
Felipe Knorr Kuhn
83a08b0f74 Merge branch 'master' into update_cypress 2022-07-06 13:35:09 -07:00
nymkappa
0887428066 Save block predictions results in db and show results in a chart 2022-07-06 22:27:45 +02:00
Felipe Knorr Kuhn
1c018d18bd Update Cypress related dependencies 2022-07-06 13:24:04 -07:00
wiz
8040abaec4 Merge pull request #1940 from mempool/nymkappa/feature/price-columns
Replace json `prices.avg_prices` with table columns - update prices logs
2022-07-06 22:22:11 +02:00
Felipe Knorr Kuhn
a2e2b36a76 Remove frontend dependencies and old tests 2022-07-06 13:13:59 -07:00
wiz
f17998cfce Merge branch 'master' into nymkappa/feature/price-columns 2022-07-06 22:13:46 +02:00
wiz
421375ba62 Merge pull request #1947 from hunicus/vb-wu-svb-faq-2
Add faq on vb, wu, and sat/vb
2022-07-06 22:13:40 +02:00
wiz
db1289f985 Merge branch 'master' into nymkappa/feature/price-columns 2022-07-06 22:13:25 +02:00
wiz
f5271bc7b4 Merge pull request #1935 from mempool/nymkappa/bugfix/handle-error-pool-hashrate
Handle pool dominance query error with 0 indexed blocks
2022-07-06 22:02:25 +02:00
wiz
1117324a7b Fix typo in weight units FAQ 2022-07-06 21:58:35 +02:00
wiz
57276b7abd Merge branch 'master' into nymkappa/bugfix/handle-error-pool-hashrate 2022-07-06 21:54:14 +02:00
wiz
b0c334fbe3 Merge pull request #1928 from mempool/nymkappa/feature/hashrate-resolution
Add resolution scaling to hashrate and difficulty chart
2022-07-06 21:52:49 +02:00
nymkappa
7a8fa6e056 Remove debug log 2022-07-06 21:41:12 +02:00
Felipe Knorr Kuhn
72c4ea0065 Fix crypto library usage for the Electrum API 2022-07-06 12:34:54 -07:00
Felipe Knorr Kuhn
1805b74edf Add types for cryptojs 2022-07-06 12:25:30 -07:00
wiz
327b2aa070 Merge branch 'master' into nymkappa/feature/hashrate-resolution 2022-07-06 21:10:18 +02:00
wiz
fdc3e7a95f Merge pull request #1938 from mempool/simon/npm-upgrade-0626
Upgrading npm packages
2022-07-06 21:09:42 +02:00
nymkappa
9ed7b2aad3 Add hashrate & difficulty chart resolution scaling 2022-07-06 21:03:55 +02:00
softsimon
42188dcef5 Upgrade packages - backend 2022-07-06 20:47:10 +02:00
softsimon
2b2f4f05b6 Upgrading frontend npm packages 2022-07-06 20:47:10 +02:00
wiz
f5dab6f215 Merge pull request #1931 from TechMiX/rtlFixes3
fix various RTL layout issues
2022-07-06 20:29:06 +02:00
wiz
2911fbe5e4 Merge branch 'master' into rtlFixes3 2022-07-06 20:09:04 +02:00
wiz
92d7519d8d Merge pull request #1930 from mempool/nymkappa/bugfix/retry-indexing-upon-error
If indexing fails try again in 10 seconds
2022-07-06 20:08:58 +02:00
wiz
e94938d5dd Merge branch 'master' into nymkappa/bugfix/retry-indexing-upon-error 2022-07-06 19:29:31 +02:00
Felipe Knorr Kuhn
950d874b9b Merge branch 'master' into mining_e2e_tests_v2 2022-07-06 10:22:09 -07:00
wiz
81c68620a1 Merge pull request #1985 from Emzy/ops/install-linux-crontab
Add crontab creation for linux and bitcoin-minfee.service in prod install
2022-07-06 19:02:27 +02:00
Felipe Knorr Kuhn
9d832f9bfc Add initial mining dashboard tests 2022-07-06 10:01:05 -07:00
Felipe Knorr Kuhn
460ff68a52 Add test locators to the mining dashboard elements 2022-07-06 10:00:32 -07:00
wiz
96007509b5 Merge pull request #1979 from Emzy/ops/cpu-count
Limit make to max cpus in prod install
2022-07-06 18:51:53 +02:00
wiz
bb74a25adc Merge pull request #1977 from Emzy/ops/fix-shebang-scripts
Disable confirmation prompt for rust and change shebang in scripts to be universal for prod install
2022-07-06 18:51:18 +02:00
wiz
c36cad4619 Merge pull request #1983 from mempool/ops/nginx-refactor-for-lightning
Refactor nginx.conf and other ops scripts for lightning
2022-07-06 18:38:16 +02:00
Stephan Oeste
989c74699f Add crontab creation for linux and bitcoin-minfee.service in prod install 2022-07-06 18:31:31 +02:00
wiz
fa92ba4478 Refactor nginx.conf and other ops scripts for lightning 2022-07-06 17:13:09 +02:00
Stephan Oeste
e8829e21e7 Limit make to max cpus in prod install 2022-07-06 14:41:11 +02:00
TechMiX
d61e599de0 revert right alignments on transaction page 2022-07-06 15:29:52 +04:30
Stephan Oeste
4a6f3e189d Change shebang in scripts to be universal for prod install 2022-07-06 12:11:13 +02:00
Stephan Oeste
7154d755c1 Disable confirmation prompt for rust install in prod install script 2022-07-06 11:46:46 +02:00
nymkappa
307ee50798 Handle gracefuly query to pool dominance when there is no indexed block #1913 2022-07-06 11:00:41 +02:00
nymkappa
e8175a90f4 Replace json prices.avg_prices with table columns - update prices logs 2022-07-06 10:46:00 +02:00
nymkappa
bbc9df486e If any indexing fails | chain of hash invalid, try again in 10 seconds 2022-07-06 10:36:26 +02:00
wiz
c7014fc6c8 Merge branch 'master' into breathe-effect-framerate 2022-07-06 01:24:59 +02:00
wiz
c22aee5e60 Merge pull request #1934 from mempool/nymkappa/bugfix/show-all-timespan
Remove the condition to show `All` timespan on mining charts
2022-07-06 01:23:57 +02:00
wiz
feeb93b298 Merge pull request #1936 from mempool/nymkappa/bugfix/umbrel-electrs-504
Handle electrs error 504
2022-07-06 01:22:35 +02:00
wiz
a74dace594 Merge pull request #1925 from mempool/feature/nymkappa/index-difficulty-adjustments
Index difficulty adjustments
2022-07-06 01:19:53 +02:00
wiz
e0e2a2a626 Merge branch 'master' into rtlFixes3 2022-07-06 00:24:31 +02:00
wiz
7424c65430 Merge branch 'master' into feature/nymkappa/index-difficulty-adjustments 2022-07-06 00:20:39 +02:00
wiz
c17cf308d4 Merge pull request #1969 from mempool/knorrium/eslint
Add eslint to the backend and frontend
2022-07-06 00:20:03 +02:00
wiz
bb3f7fe61f Merge branch 'master' into knorrium/eslint 2022-07-06 00:12:39 +02:00
wiz
f7fcc82933 Merge pull request #1973 from Emzy/ops/linux-no-patch-electrs
Only patch electrs for FreeBSD in prod install script
2022-07-06 00:12:26 +02:00
wiz
351d9864fe Merge branch 'master' into ops/linux-no-patch-electrs 2022-07-06 00:10:19 +02:00
wiz
8148e9a36d Merge pull request #1972 from Emzy/ops/linux-java-install
Ops/linux install java Bitcoin core and Liquid in prod install script
2022-07-06 00:09:32 +02:00
wiz
34195f0e45 Fix typo in prod installer 2022-07-06 00:09:05 +02:00
wiz
9b529d075a Fix typo in prod installer 2022-07-06 00:08:55 +02:00
wiz
38a98f70d9 Fix typo in prod installer 2022-07-06 00:08:47 +02:00
wiz
544ab890b0 Merge branch 'master' into ops/linux-java-install 2022-07-06 00:05:22 +02:00
wiz
d0ad4742c1 Merge pull request #1971 from Emzy/ops/linux-services
Install packages, Bitcoin and Liquid core services in prod install script
2022-07-06 00:05:11 +02:00
wiz
ee5f7600dc Merge branch 'master' into ops/linux-services 2022-07-06 00:04:11 +02:00
wiz
5b400daf3b Merge pull request #1970 from Emzy/ops/linux-packages-fix
Fix Linux packages, usernames and path in prod install script
2022-07-06 00:03:59 +02:00
wiz
58882136a0 Merge pull request #1968 from Emzy/ops/linux-sed-fix
Fix sed command for Linux systems in the prod install script
2022-07-06 00:02:36 +02:00
wiz
1ebf089d37 Merge pull request #1901 from mempool/simon/block-tip-hash-api
Adding missing Block Tip Hash API
2022-07-06 00:01:44 +02:00
wiz
1499eb3ba8 Merge branch 'master' into simon/block-tip-hash-api 2022-07-05 23:48:20 +02:00
wiz
67adf4c310 Merge pull request #1885 from mempool/nymkappa/bugfix/indexer-not-running
Don't run the indexer unless Bitcoin Core is fully synced
2022-07-05 23:48:05 +02:00
wiz
cd4ced8d6d Merge branch 'master' into nymkappa/bugfix/indexer-not-running 2022-07-05 23:02:05 +02:00
nymkappa
acfdc8163b Index difficulty adjustments 2022-07-05 16:52:56 +02:00
Stephan Oeste
df73548f7e Removing commands and enable tor in prod install script 2022-07-05 14:43:01 +02:00
Felipe Knorr Kuhn
f4389e11ba Change the CI GHA to a matrix job 2022-07-05 05:37:17 -07:00
Stephan Oeste
f8c6a7c77b Omit FreeBSD Elecrs patch for Linux on prod install script 2022-07-05 14:20:35 +02:00
Stephan Oeste
9c65ff3e12 Fix Bisq Java install script name for prod install script 2022-07-05 14:09:32 +02:00
Stephan Oeste
79a90aeec2 Install Bitcoin and Liquid core services in prod install script 2022-07-05 13:54:58 +02:00
Felipe Knorr Kuhn
8e8609371f Fix GHA syntax 2022-07-05 04:48:44 -07:00
Felipe Knorr Kuhn
174976ce82 Fix GHA syntax 2022-07-05 04:46:13 -07:00
Felipe Knorr Kuhn
57adce693a Checkout the repo to dev and prod dirs to run different test targets 2022-07-05 04:42:04 -07:00
Stephan Oeste
d9576bb2e4 Fix Linux packages, usernames and path in prod install script 2022-07-05 13:39:29 +02:00
Felipe Knorr Kuhn
97686e1c87 Add working directory to the lint stage on GHA 2022-07-05 04:34:43 -07:00
Felipe Knorr Kuhn
35db3ffbf0 Use absolute paths when running eslint 2022-07-05 04:31:10 -07:00
Felipe Knorr Kuhn
0a747b5609 Enable running eslint on GHA 2022-07-05 04:30:56 -07:00
Felipe Knorr Kuhn
e947f3259e Fix eslint errors 2022-07-05 04:30:05 -07:00
Felipe Knorr Kuhn
a16eb6e804 Add eslint to the frontend 2022-07-05 04:29:46 -07:00
Felipe Knorr Kuhn
54334a1854 Add eslint to the backend 2022-07-05 04:29:11 -07:00
Stephan Oeste
a34eb9ba88 Fix sed command for Linux systems in the prod install script 2022-07-05 13:19:56 +02:00
Antoni Spaanderman
f218efbeb2 set error correction level to low for qr codes 2022-07-05 10:38:31 +02:00
wiz
570d8cfc74 Merge pull request #1948 from hunicus/swp-gitignore
Tell git to ignore .swp files
2022-07-04 19:58:22 +02:00
wiz
d964ccca12 Merge pull request #1942 from hunicus/doc-tab-animation
Remove fade animation when switching docs tabs
2022-07-04 19:56:17 +02:00
wiz
746e205d74 Merge pull request #1944 from hunicus/feerate-tooltip-usually
Add 'usually' to feerate tooltips
2022-07-04 19:55:54 +02:00
wiz
ad29462a6d Merge pull request #1965 from Emzy/ops/linux-dir-creation
Create directories for Linux in prod install
2022-07-04 19:55:09 +02:00
wiz
d6cd17e4c8 Merge pull request #1964 from Emzy/ops/linux-user-creation
User creation for linux in the prod install script
2022-07-04 19:54:34 +02:00
wiz
1383c20703 Merge pull request #1963 from Emzy/ops/freebsd-python-version
Update python version for FreeBSD 13 in prod install
2022-07-04 19:53:36 +02:00
wiz
5fc91fe466 Merge pull request #1958 from Emzy/emzy/fix-dialog-error
Fixed dialog error in production installer
2022-07-04 19:53:14 +02:00
Felipe Knorr Kuhn
77d9cba468 Merge branch 'master' into doc-tab-animation 2022-07-04 09:28:04 -07:00
Felipe Knorr Kuhn
32cd93b689 Merge branch 'master' into simon/block-error-code 2022-07-04 09:21:03 -07:00
Felipe Knorr Kuhn
083634826e Merge branch 'master' into breathe-effect-framerate 2022-07-04 09:10:56 -07:00
Felipe Knorr Kuhn
29557ddd86 Merge branch 'master' into emzy/fix-dialog-error 2022-07-04 09:10:38 -07:00
Stephan Oeste
f80b97af53 Create directories for Linux in prod install 2022-07-04 17:58:36 +02:00
softsimon
543c1cee62 Merge pull request #1946 from knorrium/knorrium/gha_build
Various typing and build fixes to enable CI
2022-07-04 17:57:31 +02:00
Felipe Knorr Kuhn
3c2171efb3 Fix WebSocket type error 2022-07-04 08:37:36 -07:00
Stephan Oeste
a7b28ca8e8 User creation for linux in the prod install script 2022-07-04 16:57:20 +02:00
Stephan Oeste
383e3e55a5 Update python version for FreeBSD 13 in prod install 2022-07-04 16:29:09 +02:00
wiz
8660dc3eba Merge pull request #1962 from knorrium/ops/skip_tests_for_ops
Skip tests if branch name starts with ops
2022-07-04 16:10:53 +02:00
Felipe Knorr Kuhn
00bb09faaa Skip tests if branch name starts with ops 2022-07-04 02:55:21 -07:00
wiz
f13c8b36cd Merge pull request #1960 from mempool/ops/add-lnd-to-prod-installer
Enable zmq for bitcoind, fix ./configure opts
2022-07-04 10:57:43 +02:00
Stephan Oeste
e4ac09ea57 Fixed dialog error in production installer 2022-07-03 17:11:25 +02:00
wiz
72492c9b39 Enable zmq for bitcoind, fix ./configure opts 2022-07-03 16:09:29 +02:00
wiz
39b74a42e5 Merge pull request #1955 from mempool/wiz/upgrade-bitcoin-core-v23
[ops] Upgrade prod install to use Bitcoin v23
2022-07-03 19:41:40 +09:00
wiz
cdd2d9089b Merge branch 'master' into wiz/upgrade-bitcoin-core-v23 2022-07-03 19:34:22 +09:00
wiz
e086daeecb Merge pull request #1923 from knorrium/knorrium/skip_tests_on_ops_tag
Skip tests when the ops label is used
2022-07-03 19:33:15 +09:00
Felipe Knorr Kuhn
7f5ddaf930 Ignore pushes to the ops branches 2022-07-03 03:29:09 -07:00
wiz
0f39b3b7d0 [ops] Upgrade prod install to use Bitcoin v23 2022-07-03 12:31:18 +04:00
softsimon
bae43249b2 Block error status code fix 2022-07-02 18:50:04 +02:00
hunicus
7f01bda06d Tell git to ignore .swp files 2022-06-30 00:29:31 -04:00
Mononaut
2c73153db0 limit pulsing blocks animation frame rate to 30FPS
possibly resolves #1941
2022-06-29 16:09:04 +00:00
hunicus
7e22fe1617 Add faq explaining sat/vb 2022-06-28 22:57:43 -04:00
hunicus
415ec685e6 Add faq explaining vb and wu 2022-06-28 22:57:37 -04:00
Felipe Knorr Kuhn
35f8e06ec4 Merge branch 'master' into nymkappa/bugfix/indexer-not-running 2022-06-28 06:52:04 -07:00
Felipe Knorr Kuhn
431c8c35b9 Merge branch 'master' into simon/block-tip-hash-api 2022-06-28 06:44:41 -07:00
Felipe Knorr Kuhn
57c30da40f Merge branch 'master' into rtlFixes3 2022-06-28 06:43:51 -07:00
Felipe Knorr Kuhn
9ae2cb79c6 Merge branch 'master' into nymkappa/bugfix/show-all-timespan 2022-06-28 06:19:00 -07:00
Felipe Knorr Kuhn
1b2fbfd506 Merge branch 'master' into nymkappa/bugfix/umbrel-electrs-504 2022-06-28 06:18:34 -07:00
Felipe Knorr Kuhn
db73b0f671 Merge branch 'master' into doc-tab-animation 2022-06-28 06:17:34 -07:00
Felipe Knorr Kuhn
044e786379 Merge branch 'master' into feerate-tooltip-usually 2022-06-28 06:17:12 -07:00
wiz
7262f61ca0 Merge pull request #1945 from mempool/nymkappa/bugfix/doc-component-crash 2022-06-28 22:15:40 +09:00
Felipe Knorr Kuhn
19ae01defb Various typing and build fixes to enable CI 2022-06-27 21:28:21 -07:00
hunicus
1c40a22416 Add 'usually' to feerate tooltips 2022-06-27 00:30:27 -04:00
TechMiX
ceb0050ea9 fix block hash on block page in rtl layout 2022-06-27 03:17:40 +04:30
nymkappa
b97ea010cb Fix js crash when loading faq - fix crash when switching tabs with active fragment 2022-06-27 00:15:11 +02:00
wiz
aad94a1af3 Merge branch 'master' into simon/block-tip-hash-api 2022-06-27 00:32:14 +09:00
wiz
c738816cb6 Merge pull request #1922 from mononaut/bugfix/visualization-colors
Fix block visualization color for txs with 0 < feerate < 1 sat/vb
2022-06-27 00:31:29 +09:00
wiz
f5f53c93f7 Merge branch 'master' into bugfix/visualization-colors 2022-06-27 00:08:12 +09:00
wiz
07415d3871 Merge pull request #1929 from mempool/nymkappa/bugfix/minimum-effective-fee-is-zero
Minimum transaction fee is 0
2022-06-27 00:07:52 +09:00
nymkappa
1d2841b2a6 Handle electrs error 504 2022-06-26 10:50:42 +02:00
nymkappa
4ea2a8244a Remove the condition to show All timespan on mining charts 2022-06-26 10:27:07 +02:00
TechMiX
ab0c55b0fa fix various RTL layout issues 2022-06-26 02:20:36 +04:30
nymkappa
d6f594b95a Don't run the indexer unless Bitcoin Core is fully synced 2022-06-25 19:18:32 +02:00
nymkappa
a8de738e9b Fixes the minium tx fee bug introduced in ecbd18087b 2022-06-25 19:06:26 +02:00
Mononaut
61c309cd1d Fix block visualization color for <1 sat/vb txs 2022-06-25 15:15:52 +00:00
hunicus
5b9d6a31e5 Remove fade animation when switching docs tabs 2022-06-25 10:13:18 -04:00
Felipe Knorr Kuhn
8fa0539b5a Update test triggers 2022-06-24 21:56:08 -07:00
Felipe Knorr Kuhn
7262485f3b Skip tests when the ops labeled is used 2022-06-24 21:51:13 -07:00
wiz
24300eeac5 Merge pull request #1920 from hunicus/fix-docker-typo
Fix typo in docker docs
2022-06-25 02:58:33 +09:00
hunicus
2be6c19ba2 Fix typo in docker docs 2022-06-24 13:55:26 -04:00
wiz
001bd1d442 Merge pull request #1918 from mempool/wiz/add-mononaut-repo-to-build-script
[ops] Add mononaut's fork repo to list of trusted repos
2022-06-24 18:03:46 +09:00
wiz
f30d26b83c [ops] Add mononaut's fork repo to list of trusted repos 2022-06-24 17:59:08 +09:00
wiz
198c52fd5f Merge pull request #1917 from mempool/wiz/fix-prod-nginx-perma-cache-patterns
[ops] Fix prod nginx perma-cache URL patterns
2022-06-24 17:57:33 +09:00
wiz
c5e0b0fc74 [ops] Fix prod nginx perma-cache URL patterns
Currently we perma-cache everything matching /api/block, but this
is bad because it also matches /api/block-height and /api/blocks/
API endpoints, which shouldn't be perma-cached. Add a trailing slash
to prevent those from getting matched.
2022-06-24 17:52:34 +09:00
wiz
ecefddf2c3 Merge pull request #1911 from mempool/nymkappa/bugfix/double-api-call-charts
Fix double http query when switching timespan for the first time
2022-06-24 00:26:06 +09:00
wiz
4c8eaac144 Merge branch 'master' into nymkappa/bugfix/double-api-call-charts 2022-06-23 23:53:46 +09:00
wiz
9991d43b3b Merge pull request #1912 from mempool/nymkappa/bugfix/dont-preload-genesis
Don't preload /block APIs for genesis block
2022-06-23 23:52:11 +09:00
wiz
98b9f007c6 Merge branch 'master' into nymkappa/bugfix/dont-preload-genesis 2022-06-23 23:43:29 +09:00
nymkappa
53812c3751 Don't preload /block APIs for genesis block 2022-06-23 16:29:25 +02:00
wiz
3e01207026 Merge pull request #1850 from mempool/nymkappa/feature/price-index
Compute our own price index and save into database every 1 hour
2022-06-23 23:11:17 +09:00
nymkappa
68f72e3074 Implement our own price indexer with historical data 2022-06-23 15:42:42 +02:00
nymkappa
03ade97c0e Fix double http query when switching timespan for the first time 2022-06-23 15:30:42 +02:00
wiz
411e9c2e89 Merge branch 'master' into simon/block-tip-hash-api 2022-06-23 20:50:01 +09:00
wiz
93dab57959 Merge pull request #1895 from mempool/nymkappa/feature/hashrate-moving-average
Add moving average to the hashrate & difficulty chart
2022-06-23 20:38:30 +09:00
wiz
625dba943b Merge branch 'master' into nymkappa/feature/hashrate-moving-average 2022-06-23 19:20:31 +09:00
wiz
b272d1e27e Merge pull request #1908 from mempool/simon/batch-outspends
Batch outspends requests
2022-06-23 19:20:22 +09:00
softsimon
960513c370 Fix for outspends when using esplora 2022-06-23 11:55:56 +02:00
wiz
61afa92d05 Merge branch 'master' into simon/batch-outspends 2022-06-23 18:25:15 +09:00
wiz
fa0373c181 Merge pull request #1905 from mempool/nymkappa/feature/preload-prev-block-summary
Preload the previous block summary
2022-06-23 18:25:00 +09:00
wiz
5373078a30 Merge branch 'master' into nymkappa/feature/preload-prev-block-summary 2022-06-23 18:14:48 +09:00
wiz
17f0222e47 Merge pull request #1907 from mononaut/disable-mined-block-transitions
Disable mined block animations
2022-06-23 18:13:56 +09:00
softsimon
1479039fb5 Batch outspends requests
fixes #1902
2022-06-22 23:42:20 +02:00
nymkappa
da28e7b80e Preload the previous block - Disable 300 ms on block page 2022-06-22 23:17:49 +02:00
Mononaut
f2780e65cd Disable mined block animations 2022-06-22 19:10:31 +00:00
wiz
f9a1f10b99 Merge branch 'master' into nymkappa/feature/hashrate-moving-average 2022-06-23 02:43:40 +09:00
wiz
816263bd54 Merge pull request #1893 from mempool/nymkappa/feature/remove-hide-button
Remove dashboard row collasping feature
2022-06-23 02:43:30 +09:00
wiz
eb169cf58b Merge branch 'master' into nymkappa/feature/remove-hide-button 2022-06-23 02:34:25 +09:00
wiz
2d6fcd6d67 Merge pull request #1896 from mempool/nymkappa/feature/index-block-visualization
Index block summaries in db
2022-06-22 23:28:36 +09:00
wiz
9d1883f925 [ops] Cache /api/block and /api/v1/block for 1 month 2022-06-22 23:26:41 +09:00
softsimon
85e544dc8e Adding missing Block Tip Hash API 2022-06-22 13:15:44 +02:00
nymkappa
aa86885e6b Set block/:hash/summary expiration to 1 month - Support re-org for block summaries 2022-06-20 16:35:10 +02:00
nymkappa
72a603ac37 Index block summaries in db 2022-06-18 16:48:02 +02:00
nymkappa
a5d9d5e575 Add moving average to the hashrate & difficulty chart 2022-06-18 10:22:38 +02:00
wiz
95d645255d Merge pull request #1892 from mononaut/mined-block-visualization 2022-06-18 09:57:52 +09:00
nymkappa
6c0fe3d7a1 Remove dashboard row collasping feature 2022-06-17 11:15:38 +02:00
wiz
071d3e65a3 Merge pull request #1887 from mempool/nymkappa/feature/chart-timespan-url
Add #{timespan} in mining charts urls for easier sharing
2022-06-17 08:30:53 +09:00
wiz
95323ac4cb Merge branch 'master' into nymkappa/feature/chart-timespan-url 2022-06-17 08:19:56 +09:00
wiz
f7b60f3da7 Merge pull request #1882 from mempool/nymkappa/bugfix/pool-ranking
Fix pool ranking layout on small mobile device
2022-06-17 08:19:48 +09:00
wiz
806a30c3d8 Merge branch 'master' into nymkappa/bugfix/pool-ranking 2022-06-17 08:08:37 +09:00
wiz
74570676b5 Merge pull request #1881 from hunicus/fix-anchors-ngfaq
Fix anchor links and typos in faq
2022-06-17 08:08:28 +09:00
wiz
903471ee43 Merge pull request #1883 from mempool/nymkappa/bugfix/remove-bottom-label-charts-dashboard
Hide bottom axis label for mempool and incoming tx charts on widgets
2022-06-17 08:02:19 +09:00
wiz
9a54a94dca Merge branch 'master' into nymkappa/bugfix/remove-bottom-label-charts-dashboard 2022-06-17 07:49:03 +09:00
wiz
532b7a430c Merge pull request #1884 from mempool/nymkappa/feature/widget-title-links
Make `Latest blocks` and `Adjustments` widget title clickable
2022-06-17 07:48:52 +09:00
wiz
7e08058d0a Merge pull request #1889 from hunicus/regtest-formatting
Edit regtest docs formatting
2022-06-17 07:44:04 +09:00
wiz
47d84d4ab6 Merge pull request #1890 from hunicus/docker-video-link
Add k3tan video link to docker readme
2022-06-17 07:43:01 +09:00
Mononaut
288bddcaf2 Add API endpoint for block summary data 2022-06-16 02:46:27 +00:00
Mononaut
2d529bd581 Tooltip-style tx previews in block overview 2022-06-16 02:46:27 +00:00
Mononaut
300f5375c8 Optimize block visualization rendering 2022-06-16 02:46:27 +00:00
Mononaut
7f4c6352ba Add visualization to mined blocks 2022-06-16 02:46:24 +00:00
Mononaut
225decd286 Extract canvas/webgl code to separate component 2022-06-15 19:59:24 +00:00
hunicus
539d41f19e Add k3tan video link to docker readme 2022-06-15 15:51:11 -04:00
hunicus
ca92834493 Edit formatting of regtest docs 2022-06-15 15:27:18 -04:00
nymkappa
d28fe93360 Add #{timespan} in mining charts urls for easier sharing 2022-06-15 11:26:58 +02:00
wiz
6ff69c0fa8 Merge branch 'master' into nymkappa/bugfix/remove-bottom-label-charts-dashboard 2022-06-15 08:29:48 +09:00
wiz
0409c9a9c0 Merge branch 'master' into nymkappa/feature/widget-title-links 2022-06-15 08:09:41 +09:00
wiz
c5bcf76353 Merge pull request #1867 from mempool/nymkappa/feature/add-tooltip-pool-ranking
Only show `(1w)` on `Blocks` in pool ranking widget and add tooltips
2022-06-15 08:09:29 +09:00
nymkappa
20a4b9fb5a Make Latest blocks and Adjustments widget title clickable 2022-06-14 10:35:29 +02:00
nymkappa
9d20637dcb Hide bottom axis label for mempool and incoming tx charts on widgets 2022-06-14 09:58:32 +02:00
nymkappa
da4efdb2d0 Fix pool ranking layout on small mobile device 2022-06-14 09:42:56 +02:00
nymkappa
9fe4cc2d2b Only show (1w) on Blocks in pool ranking widget
Add tooltips to explain `Pools luck`, `Pools count` and `Blocs (1w)`
2022-06-14 08:58:31 +02:00
hunicus
b82abc2827 Make minor faq updates
Add Start9 and capitalize Mempool.
2022-06-13 21:11:57 -04:00
hunicus
8de1fb5289 Fix stray typos from moving faqs to html 2022-06-13 21:08:44 -04:00
hunicus
0031fbf886 Fix anchor links on new ng faq template 2022-06-13 21:03:17 -04:00
wiz
e24efe7528 Merge pull request #1878 from mempool/nymkappa/feature/regtest-notes
Added regtest notes to backend README
2022-06-14 08:32:09 +09:00
wiz
109de73691 Fix typo in backend/README.md 2022-06-14 08:31:48 +09:00
wiz
dc475462d0 Merge pull request #1877 from mempool/nymkappa/bugfix/da-mining-dashboard
Listen for `stats` websocket event in the mining dashboard to get latest DA
2022-06-14 08:30:37 +09:00
wiz
19883c03ad Merge pull request #1876 from mempool/nymkappa/bugfix/cleanup-log
Cleanup some ops logs
2022-06-14 08:29:21 +09:00
wiz
46ae76081d Merge branch 'master' into nymkappa/bugfix/cleanup-log 2022-06-14 07:41:28 +09:00
wiz
63a931a10a Merge branch 'master' into nymkappa/bugfix/da-mining-dashboard 2022-06-14 07:39:17 +09:00
wiz
a15da76566 Merge pull request #1874 from mempool/simon/faq-html-templates
Move FAQ HTML to angular templates
2022-06-14 07:38:21 +09:00
wiz
465053f3ff Merge branch 'master' into simon/faq-html-templates 2022-06-14 07:28:58 +09:00
wiz
bf99407816 Merge pull request #1873 from mononaut/fix-retina-resolution
Fix canvas resolution on high-DPI screens
2022-06-14 07:05:28 +09:00
nymkappa
0493d57b2e Added regtest notes to backend README 2022-06-13 11:28:34 +02:00
nymkappa
947864cff8 Listen for stats websocket event in the mining dashboard to get latest DA 2022-06-13 11:13:00 +02:00
nymkappa
1074d23a90 Cleanup some ops logs 2022-06-13 10:12:27 +02:00
softsimon
24ffc97317 Move FAQ HTML to angular templates
fixes #1783
2022-06-12 22:52:49 +02:00
Mononaut
1f6b59f2f5 Fix canvas resolution on high-DPI screens 2022-06-12 18:26:19 +00:00
wiz
5314eb2d45 Merge pull request #1866 from mempool/nymkappa/bugfix/blocks-list-signet
Fix latest blocks rewards column not large enough to fit high reward
2022-06-12 05:17:38 +09:00
wiz
035b29e70b Merge branch 'master' into nymkappa/bugfix/blocks-list-signet 2022-06-12 05:12:02 +09:00
wiz
68ec7bce12 Merge pull request #1864 from mempool/nymkappa/feature/cleanup-chart-tooltips
Make all mining charts tooltips uniform
2022-06-12 04:56:15 +09:00
wiz
02b34c9811 Merge branch 'master' into nymkappa/feature/cleanup-chart-tooltips 2022-06-12 04:34:58 +09:00
wiz
99fcca3cb7 Merge pull request #1870 from hunicus/fix-anchors-chrome
Fix anchor link scrolling on load (chrome)
2022-06-12 03:34:11 +09:00
wiz
fc2ff27928 Merge pull request #1869 from hunicus/dev-only
Clarify that manual installs are meant for devs
2022-06-12 01:22:48 +09:00
hunicus
abaaef2285 Fix anchor link scrolling on load (chrome) 2022-06-11 12:21:08 -04:00
hunicus
8154a4dd77 Clarify that manual installs are meant for devs 2022-06-11 11:05:53 -04:00
wiz
6bbea198e5 Merge pull request #1863 from mempool/nymkappa/bugfix/i18n-missing
Replace "Indexing in progress" with localized "Indexing blocks"
2022-06-11 06:57:28 +09:00
nymkappa
2d569b8bcf Fix latest blocks rewards column not large enough to fit high reward 2022-06-10 23:53:53 +02:00
nymkappa
da3272df76 Make all mining charts tooltips uniform 2022-06-10 23:34:13 +02:00
nymkappa
3243b1a3cb Replace "Indexing in progress" with localized "Indexing blocks" 2022-06-10 23:29:27 +02:00
wiz
2492bc69ff Merge pull request #1860 from hunicus/npm-install-note
Update npm install command and node.js requirements
2022-06-11 05:23:08 +09:00
wiz
feb1c051e1 Merge pull request #1841 from hunicus/api-limit-note
Add note on rate limits to api docs
2022-06-11 02:11:49 +09:00
wiz
6fb57cb1a9 Merge pull request #1858 from mempool/release/v2.4.0
Release v2.4.0
2022-06-10 22:44:54 +09:00
wiz
b9f0e63341 Bump version numbers to v2.4.1-dev 2022-06-10 22:43:57 +09:00
hunicus
ca41edea22 Update npm install command 2022-06-09 18:15:20 -04:00
wiz
f901f06992 Release v2.4.0 2022-06-09 23:10:18 +09:00
wiz
6038e04ccc Merge pull request #1857 from mempool/i18n/pull-from-transifex-2206092307
Pull from transifex 202206092308
2022-06-09 23:08:42 +09:00
wiz
4e792018ac Pull from transifex 202206092308 2022-06-09 23:08:07 +09:00
wiz
80700fa031 Merge pull request #1855 from mempool/nymkappa/feature/ellipsis
Use ellipsis + tooltip combo to fix layout on long string
2022-06-09 23:07:36 +09:00
nymkappa
aa02170e5c Use ellipsis + tooltip combo to fix layout on long string 2022-06-09 15:58:49 +02:00
wiz
bb7bd1a504 Merge pull request #1856 from mempool/wiz/pull-from-transifex-202206092208
Pull from transifex 202206092208
2022-06-09 22:09:47 +09:00
wiz
04d1b8e7c2 Pull from transifex 202206092208 2022-06-09 22:09:11 +09:00
wiz
435f9358b1 Merge pull request #1854 from mempool/i18n/pull-from-transifex-202206092053
Pull from transifex 202206092024
2022-06-09 20:25:45 +09:00
wiz
0d22bf5cae Pull from transifex 202206092024 2022-06-09 20:24:11 +09:00
wiz
044c233598 Merge pull request #1853 from mempool/i18n/pull-from-transifex-202206091947
Pull from transifex 202206091948
2022-06-09 19:48:44 +09:00
wiz
c160ff9d27 Pull from transifex 202206091948 2022-06-09 19:48:02 +09:00
wiz
915adf6397 Merge pull request #1852 from mempool/i18n/pull-from-transifex-202206091724
Pull from transifex 202206091724
2022-06-09 17:29:18 +09:00
wiz
c7ed2ed59d Pull from transifex 202206091724 2022-06-09 17:24:43 +09:00
wiz
9b91274cf2 Merge pull request #1849 from mempool/simon/transifex-pull-08062
Pull from transifex
2022-06-09 03:23:33 +09:00
softsimon
e5ee6bd6eb Pull from transifex 2022-06-08 20:16:30 +02:00
wiz
e1611d1e18 Merge branch 'master' into api-limit-note 2022-06-08 23:52:08 +09:00
wiz
60c22cbb5d Merge pull request #1844 from mempool/wiz/fix-docker-backend-start-script
Fix one more broken sed substitution in docker/backend/start.sh
2022-06-08 23:38:13 +09:00
wiz
c7dd93275e Merge pull request #1847 from mempool/simon/transifex-pull-806
Transifex pull
2022-06-08 23:37:11 +09:00
softsimon
3f0201df3a Transifex pull 2022-06-08 16:14:36 +02:00
wiz
bce8a58cf8 Fix one more broken sed substitution in docker/backend/start.sh 2022-06-08 22:13:56 +09:00
hunicus
752156281f Show rate limiting note on official instances only
Also fix some styling flaws resulting from hiding
the rate limiting text.
2022-06-07 23:27:35 -04:00
hunicus
0073322758 Make rest api doc section welcomer full sentence 2022-06-07 22:55:29 -04:00
hunicus
ca3ca4557e Add note on api limits 2022-06-07 22:52:08 -04:00
hunicus
056a9980d6 Clean up styling on doc content container 2022-06-07 22:36:17 -04:00
wiz
936964d273 Merge pull request #1839 from mempool/wiz/fix-docker-backend-start-script
Add missing sed substitutions in docker/backend/start.sh
2022-06-08 08:05:41 +09:00
wiz
4d274a3cec Add missing sed substitutions in docker/backend/start.sh
Fixes #1838
2022-06-08 07:50:21 +09:00
606 changed files with 65503 additions and 19137 deletions

12
.github/FUNDING.yml vendored
View File

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

96
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,96 @@
name: CI Pipeline for the Backend and Frontend
on:
pull_request:
types: [opened, review_requested, synchronize]
jobs:
backend:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
strategy:
matrix:
node: ["16.16.0", "18.5.0"]
flavor: ["dev", "prod"]
fail-fast: false
runs-on: "ubuntu-latest"
name: Backend (${{ matrix.flavor }}) - node ${{ matrix.node }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
path: ${{ matrix.node }}/${{ matrix.flavor }}
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
registry-url: "https://registry.npmjs.org"
- name: Install
if: ${{ matrix.flavor == 'dev'}}
run: npm ci
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
- name: Install (Prod dependencies only)
if: ${{ matrix.flavor == 'prod'}}
run: npm ci --omit=dev --omit=optional
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
- name: Lint
if: ${{ matrix.flavor == 'dev'}}
run: npm run lint
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
- name: Unit Tests
if: ${{ matrix.flavor == 'dev'}}
run: npm run test
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
- name: Build
run: npm run build
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
frontend:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
strategy:
matrix:
node: ["16.15.0", "18.5.0"]
flavor: ["dev", "prod"]
fail-fast: false
runs-on: "ubuntu-latest"
name: Frontend (${{ matrix.flavor }}) - node ${{ matrix.node }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
path: ${{ matrix.node }}/${{ matrix.flavor }}
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
registry-url: "https://registry.npmjs.org"
- name: Install (Prod dependencies only)
run: npm ci --omit=dev --omit=optional
if: ${{ matrix.flavor == 'prod'}}
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
- name: Install
if: ${{ matrix.flavor == 'dev'}}
run: npm ci
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
- name: Lint
if: ${{ matrix.flavor == 'dev'}}
run: npm run lint
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
# - name: Test
# run: npm run test
- name: Build
run: npm run build
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend

View File

@@ -1,88 +1,58 @@
name: Cypress Tests
on: [push, pull_request]
on:
pull_request:
types: [ opened, review_requested, synchronize ]
jobs:
cypress:
runs-on: ${{ matrix.os }}
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
runs-on: "ubuntu-latest"
strategy:
fail-fast: false
matrix:
containers: [1, 2, 3, 4, 5]
os: ["ubuntu-latest"]
browser: [chrome]
name: E2E tests on ${{ matrix.browser }} - ${{ matrix.os }}
module: ["mempool", "liquid", "bisq"]
include:
- module: "mempool"
spec: |
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
cypress/e2e/testnet/*.spec.ts
- module: "liquid"
spec: |
cypress/e2e/liquid/liquid.spec.ts
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
- module: "bisq"
spec: |
cypress/e2e/bisq/bisq.spec.ts
name: E2E tests for ${{ matrix.module }}
steps:
- name: Checkout
uses: actions/checkout@v2
with:
path: ${{ matrix.module }}
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 16.15.0
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: ${{ matrix.browser }} browser tests (Mempool)
uses: cypress-io/github-action@v4
with:
tag: ${{ github.event_name }}
working-directory: frontend
build: npm run config:defaults:mempool
start: npm run start:local-staging
wait-on: 'http://localhost:4200'
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
cypress/e2e/testnet/*.spec.ts
group: Tests on ${{ matrix.browser }} (Mempool)
browser: ${{ matrix.browser }}
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
env:
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json
- name: ${{ matrix.browser }} browser tests (Liquid)
- name: Chrome browser tests (${{ matrix.module }})
uses: cypress-io/github-action@v4
if: always()
with:
tag: ${{ github.event_name }}
working-directory: frontend
build: npm run config:defaults:liquid
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:${{ matrix.module }}
start: npm run start:local-staging
wait-on: 'http://localhost:4200'
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/liquid/liquid.spec.ts
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
group: Tests on ${{ matrix.browser }} (Liquid)
browser: ${{ matrix.browser }}
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
env:
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
- name: ${{ matrix.browser }} browser tests (Bisq)
uses: cypress-io/github-action@v4
if: always()
with:
tag: ${{ github.event_name }}
working-directory: frontend
build: npm run config:defaults:bisq
start: npm run start:local-staging
wait-on: 'http://localhost:4200'
wait-on-timeout: 120
record: true
parallel: true
spec: cypress/e2e/bisq/bisq.spec.ts
group: Tests on ${{ matrix.browser }} (Bisq)
browser: ${{ matrix.browser }}
spec: ${{ matrix.spec }}
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
env:
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}

View File

@@ -1,7 +1,7 @@
name: Docker build on tag
env:
DOCKER_CLI_EXPERIMENTAL: enabled
TAG_FMT: '^refs/tags/(((.?[0-9]+){3,4}))$'
TAG_FMT: "^refs/tags/(((.?[0-9]+){3,4}))$"
DOCKER_BUILDKIT: 0
COMPOSE_DOCKER_CLI_BUILD: 0
@@ -21,16 +21,46 @@ jobs:
service:
- frontend
- backend
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
timeout-minutes: 120
name: Build and push to DockerHub
steps:
# Workaround based on JonasAlfredsson/docker-on-tmpfs@v1.0.1
- name: Replace the current swap file
shell: bash
run: |
sudo swapoff /mnt/swapfile
sudo rm -v /mnt/swapfile
sudo fallocate -l 10G /mnt/swapfile
sudo chmod 600 /mnt/swapfile
sudo mkswap /mnt/swapfile
sudo swapon /mnt/swapfile
- name: Show current memory and swap status
shell: bash
run: |
sudo free -h
echo
sudo swapon --show
- name: Mount a tmpfs over /var/lib/docker
shell: bash
run: |
if [ ! -d "/var/lib/docker" ]; then
echo "Directory '/var/lib/docker' not found"
exit 1
fi
sudo mount -t tmpfs -o size=10G tmpfs /var/lib/docker
sudo systemctl restart docker
sudo df -h | grep docker
- 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: Add SHORT_SHA env property with commit short sha
run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@ sitemap
data
docker-compose.yml
backend/mempool-config.json
*.swp

2
.nvmrc
View File

@@ -1 +1 @@
v16.15.0
v16.16.0

View File

@@ -6,6 +6,8 @@ In order to clarify the intellectual property license granted with Contributions
When submitting a pull request for the first time, please create a file with a name like `/contributors/{github_username}.txt`, and in the content of that file indicate your agreement to the Contributor License Agreement terms below. An example of what that file should contain can be seen in wiz's agreement file. (This method of CLA "signing" is borrowed from Medium's open source project.)
Also, please GPG-sign all your commits (`git config commit.gpgsign true`).
# Contributor License Agreement
Last Updated: January 25, 2022

View File

@@ -29,5 +29,5 @@ Mempool can be conveniently installed on the following full-node distros:
Mempool can be installed in other ways too, but we only recommend doing so if you're a developer, have experience managing servers, or otherwise know what you're doing.
- See the [`docker/`](./docker/) directory for instructions on deploying Mempool with Docker.
- See the [`backend/`](./backend/) and [`frontend/`](./frontend/) directories for manual install instructions oriented for developers and small-scale deployments.
- See the [`production/`](./production/) directory for guidance on setting up a more serious Mempool instance designed for high performance at scale.
- See the [`backend/`](./backend/) and [`frontend/`](./frontend/) directories for manual install instructions oriented for developers.
- See the [`production/`](./production/) directory for guidance on setting up a more serious Mempool instance designed for high performance at scale.

17
backend/.editorconfig Normal file
View File

@@ -0,0 +1,17 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

2
backend/.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
node_modules
dist

37
backend/.eslintrc Normal file
View File

@@ -0,0 +1,37 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"@typescript-eslint/ban-ts-comment": 1,
"@typescript-eslint/ban-types": 1,
"@typescript-eslint/no-empty-function": 1,
"@typescript-eslint/no-explicit-any": 1,
"@typescript-eslint/no-inferrable-types": 0,
"@typescript-eslint/no-namespace": 1,
"@typescript-eslint/no-this-alias": 1,
"@typescript-eslint/no-var-requires": 1,
"@typescript-eslint/explicit-function-return-type": 1,
"no-console": 1,
"no-constant-condition": 1,
"no-dupe-else-if": 1,
"no-empty": 1,
"no-prototype-builtins": 1,
"no-self-assign": 1,
"no-useless-catch": 1,
"no-var": 1,
"prefer-const": 1,
"prefer-rest-params": 1,
"quotes": [1, "single", { "allowTemplateLiterals": true }],
"semi": 1,
"eqeqeq": 1
}
}

5
backend/.gitignore vendored
View File

@@ -1,9 +1,10 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# production config and external assets
*.json
!mempool-config.template.json
!mempool-config.sample.json
mempool-config.json
pools.json
icons.json
# compiled output

2
backend/.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
node_modules
package-lock.json

6
backend/.prettierrc Normal file
View File

@@ -0,0 +1,6 @@
{
"endOfLine": "lf",
"printWidth": 80,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -1,6 +1,6 @@
# Mempool Backend
These instructions are mostly intended for developers, but can be used as a basis for personal or small-scale production setups.
These instructions are mostly intended for developers.
If you choose to use these instructions for a production setup, be aware that you will still probably need to do additional configuration for your specific OS, environment, use-case, etc. We do our best here to provide a good starting point, but only proceed if you know what you're doing. Mempool does not provide support for custom setups.
@@ -77,13 +77,13 @@ Query OK, 0 rows affected (0.00 sec)
#### Build
_Make sure to use Node.js 16.15 and npm 7._
_Make sure to use Node.js 16.10 and npm 7._
Install dependencies with `npm` and build the backend:
```
cd backend
npm install # add --prod for production
npm install
npm run build
```
@@ -110,6 +110,11 @@ Run the Mempool backend:
```
npm run start
```
You can also set env var `MEMPOOL_CONFIG_FILE` to specify a custom config file location:
```
MEMPOOL_CONFIG_FILE=/path/to/mempool-config.json npm run start
```
When it's running, you should see output like this:
@@ -159,3 +164,57 @@ nodemon src/index.ts --ignore cache/ --ignore pools.json
```
`nodemon` should be in npm's global binary folder. If needed, you can determine where that is with `npm -g bin`.
### Useful Regtest Commands
Helpful link: https://gist.github.com/System-Glitch/cb4e87bf1ae3fec9925725bb3ebe223a
Run bitcoind on regtest:
```
bitcoind -regtest -rpcport=8332
```
Create a new wallet, if needed:
```
bitcoin-cli -regtest -rpcport=8332 createwallet test
```
Load wallet (this command may take a while if you have lot of UTXOs):
```
bitcoin-cli -regtest -rpcport=8332 loadwallet test
```
Get a new address:
```
address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress)
```
Mine blocks to the previously generated address. You need at least 101 blocks before you can spend. This will take some time to execute (~1 min):
```
bitcoin-cli -regtest -rpcport=8332 generatetoaddress 101 $address
```
Send 0.1 BTC at 5 sat/vB to another address:
```
./src/bitcoin-cli -named -regtest -rpcport=8332 sendtoaddress address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress) amount=0.1 fee_rate=5
```
See more example of `sendtoaddress`:
```
./src/bitcoin-cli sendtoaddress # will print the help
```
Mini script to generate transactions with random TX fee-rate (between 1 to 100 sat/vB). It's slow so don't expect to use this to test mempool spam, except if you let it run for a long time, or maybe with multiple regtest nodes connected to each other.
```
#!/bin/bash
address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress)
for i in {1..1000000}
do
./src/bitcoin-cli -regtest -rpcport=8332 -named sendtoaddress address=$address amount=0.01 fee_rate=$(jot -r 1 1 100)
done
```
Generate block at regular interval (every 10 seconds in this example):
```
watch -n 10 "./src/bitcoin-cli -regtest -rpcport=8332 generatetoaddress 1 $address"
```

20
backend/jest.config.ts Normal file
View File

@@ -0,0 +1,20 @@
import type { Config } from "@jest/types"
const config: Config.InitialOptions = {
preset: "ts-jest",
testEnvironment: "node",
verbose: true,
automock: false,
collectCoverage: true,
collectCoverageFrom: ["./src/**/**.ts"],
coverageProvider: "babel",
coverageThreshold: {
global: {
lines: 1
}
},
setupFiles: [
"./testSetup.ts",
],
}
export default config;

View File

@@ -13,13 +13,17 @@
"INITIAL_BLOCKS_AMOUNT": 8,
"MEMPOOL_BLOCKS_AMOUNT": 8,
"INDEXING_BLOCKS_AMOUNT": 11000,
"BLOCKS_SUMMARIES_INDEXING": false,
"PRICE_FEED_UPDATE_INTERVAL": 600,
"USE_SECOND_NODE_FOR_MINFEE": false,
"EXTERNAL_ASSETS": [],
"EXTERNAL_MAX_RETRY": 1,
"EXTERNAL_RETRY_INTERVAL": 0,
"USER_AGENT": "mempool",
"STDOUT_LOG_MIN_PRIORITY": "debug"
"STDOUT_LOG_MIN_PRIORITY": "debug",
"AUTOMATIC_BLOCK_REINDEXING": false,
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json",
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master"
},
"CORE_RPC": {
"HOST": "127.0.0.1",
@@ -61,10 +65,31 @@
"ENABLED": true,
"TX_PER_SECOND_SAMPLE_PERIOD": 150
},
"MAXMIND": {
"ENABLED": false,
"GEOLITE2_CITY": "/usr/local/share/GeoIP/GeoLite2-City.mmdb",
"GEOLITE2_ASN": "/usr/local/share/GeoIP/GeoLite2-ASN.mmdb",
"GEOIP2_ISP": "/usr/local/share/GeoIP/GeoIP2-ISP.mmdb"
},
"BISQ": {
"ENABLED": false,
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
},
"LIGHTNING": {
"ENABLED": false,
"BACKEND": "lnd",
"STATS_REFRESH_INTERVAL": 600,
"GRAPH_REFRESH_INTERVAL": 600,
"LOGGER_UPDATE_INTERVAL": 30
},
"LND": {
"TLS_CERT_PATH": "tls.cert",
"MACAROON_PATH": "readonly.macaroon",
"REST_API_URL": "https://localhost:8080"
},
"CLIGHTNING": {
"SOCKET": "lightning-rpc"
},
"SOCKS5PROXY": {
"ENABLED": false,
"USE_ONION": true,

8496
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "mempool-backend",
"version": "2.4.0-dev",
"version": "2.5.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",
@@ -16,33 +16,53 @@
"mempool",
"blockchain",
"explorer",
"liquid"
"liquid",
"lightning"
],
"main": "index.ts",
"scripts": {
"ng": "./node_modules/@angular/cli/bin/ng",
"tsc": "./node_modules/typescript/bin/tsc",
"build": "npm run tsc",
"tsc": "./node_modules/typescript/bin/tsc -p tsconfig.build.json",
"build": "npm run tsc && npm run create-resources",
"create-resources": "cp ./src/tasks/price-feeds/mtgox-weekly.json ./dist/tasks && node dist/api/fetch-version.js",
"package": "npm run build && rm -rf package && mv dist package && mv node_modules package && npm run package-rm-build-deps",
"package-rm-build-deps": "(cd package/node_modules; rm -r typescript @typescript-eslint)",
"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"
"test": "./node_modules/.bin/jest --coverage",
"lint": "./node_modules/.bin/eslint . --ext .ts",
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
},
"dependencies": {
"@babel/core": "^7.18.6",
"@mempool/electrum-client": "^1.1.7",
"@types/node": "^16.11.41",
"axios": "~0.27.2",
"bitcoinjs-lib": "6.0.1",
"bitcoinjs-lib": "6.0.2",
"crypto-js": "^4.0.0",
"express": "^4.18.0",
"maxmind": "^4.3.6",
"mysql2": "2.3.3",
"node-worker-threads-pool": "^1.5.1",
"socks-proxy-agent": "^6.2.0",
"typescript": "~4.7.2",
"ws": "~8.7.0"
"socks-proxy-agent": "~7.0.0",
"typescript": "~4.7.4",
"ws": "~8.8.0"
},
"devDependencies": {
"@babel/core": "^7.18.6",
"@babel/code-frame": "^7.18.6",
"@types/compression": "^1.7.2",
"@types/ws": "~8.5.3",
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.13",
"tslint": "^6.1.0"
"@types/jest": "^28.1.4",
"@types/ws": "~8.5.3",
"@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.5",
"eslint": "^8.19.0",
"eslint-config-prettier": "^8.5.0",
"jest": "^28.1.2",
"prettier": "^2.7.1",
"ts-jest": "^28.0.5",
"ts-node": "^10.8.2"
}
}

View File

@@ -0,0 +1,108 @@
{
"MEMPOOL": {
"NETWORK": "__MEMPOOL_NETWORK__",
"BACKEND": "__MEMPOOL_BACKEND__",
"BLOCKS_SUMMARIES_INDEXING": true,
"HTTP_PORT": 1,
"SPAWN_CLUSTER_PROCS": 2,
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
"AUTOMATIC_BLOCK_REINDEXING": true,
"POLL_RATE_MS": 3,
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
"CLEAR_PROTECTION_MINUTES": 4,
"RECOMMENDED_FEE_PERCENTILE": 5,
"BLOCK_WEIGHT_UNITS": 6,
"INITIAL_BLOCKS_AMOUNT": 7,
"MEMPOOL_BLOCKS_AMOUNT": 8,
"PRICE_FEED_UPDATE_INTERVAL": 9,
"USE_SECOND_NODE_FOR_MINFEE": 10,
"EXTERNAL_ASSETS": 11,
"EXTERNAL_MAX_RETRY": 12,
"EXTERNAL_RETRY_INTERVAL": 13,
"USER_AGENT": "__MEMPOOL_USER_AGENT__",
"STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
"INDEXING_BLOCKS_AMOUNT": 14,
"POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__POOLS_JSON_URL__"
},
"CORE_RPC": {
"HOST": "__CORE_RPC_HOST__",
"PORT": 15,
"USERNAME": "__CORE_RPC_USERNAME__",
"PASSWORD": "__CORE_RPC_PASSWORD__"
},
"ELECTRUM": {
"HOST": "__ELECTRUM_HOST__",
"PORT": 16,
"TLS_ENABLED": true
},
"ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__"
},
"SECOND_CORE_RPC": {
"HOST": "__SECOND_CORE_RPC_HOST__",
"PORT": 17,
"USERNAME": "__SECOND_CORE_RPC_USERNAME__",
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__"
},
"DATABASE": {
"ENABLED": false,
"HOST": "__DATABASE_HOST__",
"SOCKET": "__DATABASE_SOCKET__",
"PORT": 18,
"DATABASE": "__DATABASE_DATABASE__",
"USERNAME": "__DATABASE_USERNAME__",
"PASSWORD": "__DATABASE_PASSWORD__"
},
"SYSLOG": {
"ENABLED": false,
"HOST": "__SYSLOG_HOST__",
"PORT": 19,
"MIN_PRIORITY": "__SYSLOG_MIN_PRIORITY__",
"FACILITY": "__SYSLOG_FACILITY__"
},
"STATISTICS": {
"ENABLED": false,
"TX_PER_SECOND_SAMPLE_PERIOD": 20
},
"BISQ": {
"ENABLED": true,
"DATA_PATH": "__BISQ_DATA_PATH__"
},
"SOCKS5PROXY": {
"ENABLED": true,
"USE_ONION": true,
"HOST": "__SOCKS5PROXY_HOST__",
"PORT": "__SOCKS5PROXY_PORT__",
"USERNAME": "__SOCKS5PROXY_USERNAME__",
"PASSWORD": "__SOCKS5PROXY_PASSWORD__"
},
"PRICE_DATA_SERVER": {
"TOR_URL": "__PRICE_DATA_SERVER_TOR_URL__",
"CLEARNET_URL": "__PRICE_DATA_SERVER_CLEARNET_URL__"
},
"EXTERNAL_DATA_SERVER": {
"MEMPOOL_API": "__EXTERNAL_DATA_SERVER_MEMPOOL_API__",
"MEMPOOL_ONION": "__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__",
"LIQUID_API": "__EXTERNAL_DATA_SERVER_LIQUID_API__",
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__",
"BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__",
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
},
"LIGHTNING": {
"ENABLED": "__LIGHTNING_ENABLED__",
"BACKEND": "__LIGHTNING_BACKEND__",
"TOPOLOGY_FOLDER": "__LIGHTNING_TOPOLOGY_FOLDER__",
"STATS_REFRESH_INTERVAL": 600,
"GRAPH_REFRESH_INTERVAL": 600,
"LOGGER_UPDATE_INTERVAL": 30
},
"LND": {
"TLS_CERT_PATH": "",
"MACAROON_PATH": "",
"REST_API_URL": "https://localhost:8080"
},
"CLIGHTNING": {
"SOCKET": "__CLIGHTNING_SOCKET__"
}
}

View File

@@ -0,0 +1,62 @@
import { calcDifficultyAdjustment, DifficultyAdjustment } from '../../api/difficulty-adjustment';
describe('Mempool Difficulty Adjustment', () => {
test('should calculate Difficulty Adjustments properly', () => {
const dt = (dtString) => {
return Math.floor(new Date(dtString).getTime() / 1000);
};
const vectors = [
[ // Vector 1
[ // Inputs
dt('2022-08-18T11:07:00.000Z'), // Last DA time (in seconds)
dt('2022-08-19T14:03:53.000Z'), // Current time (now) (in seconds)
750134, // Current block height
0.6280047707459726, // Previous retarget % (Passed through)
'mainnet', // Network (if testnet, next value is non-zero)
0, // If not testnet, not used
],
{ // Expected Result
progressPercent: 9.027777777777777,
difficultyChange: 12.562233927411782,
estimatedRetargetDate: 1661895424692,
remainingBlocks: 1834,
remainingTime: 977591692,
previousRetarget: 0.6280047707459726,
nextRetargetHeight: 751968,
timeAvg: 533038,
timeOffset: 0,
},
],
[ // Vector 2 (testnet)
[ // Inputs
dt('2022-08-18T11:07:00.000Z'), // Last DA time (in seconds)
dt('2022-08-19T14:03:53.000Z'), // Current time (now) (in seconds)
750134, // Current block height
0.6280047707459726, // Previous retarget % (Passed through)
'testnet', // Network
dt('2022-08-19T13:52:46.000Z'), // Latest block timestamp in seconds
],
{ // Expected Result is same other than timeOffset
progressPercent: 9.027777777777777,
difficultyChange: 12.562233927411782,
estimatedRetargetDate: 1661895424692,
remainingBlocks: 1834,
remainingTime: 977591692,
previousRetarget: 0.6280047707459726,
nextRetargetHeight: 751968,
timeAvg: 533038,
timeOffset: -667000, // 11 min 7 seconds since last block (testnet only)
// If we add time avg to abs(timeOffset) it makes exactly 1200000 ms, or 20 minutes
},
],
] as [[number, number, number, number, string, number], DifficultyAdjustment][];
for (const vector of vectors) {
const result = calcDifficultyAdjustment(...vector[0]);
// previousRetarget is passed through untouched
expect(result.previousRetarget).toStrictEqual(vector[0][3]);
expect(result).toStrictEqual(vector[1]);
}
});
});

View File

@@ -0,0 +1,139 @@
import * as fs from 'fs';
describe('Mempool Backend Config', () => {
beforeEach(() => {
jest.resetAllMocks();
jest.resetModules();
});
test('should return defaults when no file is present', () => {
jest.isolateModules(() => {
jest.mock('../../mempool-config.json', () => ({}), { virtual: true });
const config = jest.requireActual('../config').default;
expect(config.MEMPOOL).toStrictEqual({
NETWORK: 'mainnet',
BACKEND: 'none',
BLOCKS_SUMMARIES_INDEXING: false,
HTTP_PORT: 8999,
SPAWN_CLUSTER_PROCS: 0,
API_URL_PREFIX: '/api/v1/',
AUTOMATIC_BLOCK_REINDEXING: false,
POLL_RATE_MS: 2000,
CACHE_DIR: './cache',
CLEAR_PROTECTION_MINUTES: 20,
RECOMMENDED_FEE_PERCENTILE: 50,
BLOCK_WEIGHT_UNITS: 4000000,
INITIAL_BLOCKS_AMOUNT: 8,
MEMPOOL_BLOCKS_AMOUNT: 8,
INDEXING_BLOCKS_AMOUNT: 11000,
PRICE_FEED_UPDATE_INTERVAL: 600,
USE_SECOND_NODE_FOR_MINFEE: false,
EXTERNAL_ASSETS: [],
EXTERNAL_MAX_RETRY: 1,
EXTERNAL_RETRY_INTERVAL: 0,
USER_AGENT: 'mempool',
STDOUT_LOG_MIN_PRIORITY: 'debug',
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json'
});
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
expect(config.ESPLORA).toStrictEqual({ REST_API_URL: 'http://127.0.0.1:3000' });
expect(config.CORE_RPC).toStrictEqual({
HOST: '127.0.0.1',
PORT: 8332,
USERNAME: 'mempool',
PASSWORD: 'mempool'
});
expect(config.SECOND_CORE_RPC).toStrictEqual({
HOST: '127.0.0.1',
PORT: 8332,
USERNAME: 'mempool',
PASSWORD: 'mempool'
});
expect(config.DATABASE).toStrictEqual({
ENABLED: true,
HOST: '127.0.0.1',
SOCKET: '',
PORT: 3306,
DATABASE: 'mempool',
USERNAME: 'mempool',
PASSWORD: 'mempool'
});
expect(config.SYSLOG).toStrictEqual({
ENABLED: true,
HOST: '127.0.0.1',
PORT: 514,
MIN_PRIORITY: 'info',
FACILITY: 'local7'
});
expect(config.STATISTICS).toStrictEqual({ ENABLED: true, TX_PER_SECOND_SAMPLE_PERIOD: 150 });
expect(config.BISQ).toStrictEqual({ ENABLED: false, DATA_PATH: '/bisq/statsnode-data/btc_mainnet/db' });
expect(config.SOCKS5PROXY).toStrictEqual({
ENABLED: false,
USE_ONION: true,
HOST: '127.0.0.1',
PORT: 9050,
USERNAME: '',
PASSWORD: ''
});
expect(config.PRICE_DATA_SERVER).toStrictEqual({
TOR_URL: 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices',
CLEARNET_URL: 'https://price.bisq.wiz.biz/getAllMarketPrices'
});
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual({
MEMPOOL_API: 'https://mempool.space/api/v1',
MEMPOOL_ONION: 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
LIQUID_API: 'https://liquid.network/api/v1',
LIQUID_ONION: 'http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1',
BISQ_URL: 'https://bisq.markets/api',
BISQ_ONION: 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
});
});
});
test('should override the default values with the passed values', () => {
jest.isolateModules(() => {
const fixture = JSON.parse(fs.readFileSync(`${__dirname}/../__fixtures__/mempool-config.template.json`, 'utf8'));
jest.mock('../../mempool-config.json', () => (fixture), { virtual: true });
const config = jest.requireActual('../config').default;
expect(config.MEMPOOL).toStrictEqual(fixture.MEMPOOL);
expect(config.ELECTRUM).toStrictEqual(fixture.ELECTRUM);
expect(config.ESPLORA).toStrictEqual(fixture.ESPLORA);
expect(config.CORE_RPC).toStrictEqual(fixture.CORE_RPC);
expect(config.SECOND_CORE_RPC).toStrictEqual(fixture.SECOND_CORE_RPC);
expect(config.DATABASE).toStrictEqual(fixture.DATABASE);
expect(config.SYSLOG).toStrictEqual(fixture.SYSLOG);
expect(config.STATISTICS).toStrictEqual(fixture.STATISTICS);
expect(config.BISQ).toStrictEqual(fixture.BISQ);
expect(config.SOCKS5PROXY).toStrictEqual(fixture.SOCKS5PROXY);
expect(config.PRICE_DATA_SERVER).toStrictEqual(fixture.PRICE_DATA_SERVER);
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
});
});
});

View File

@@ -1,60 +1,37 @@
import * as fs from 'fs';
import * as os from 'os';
import logger from '../logger';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { IBackendInfo } from '../mempool.interfaces';
const { spawnSync } = require('child_process');
class BackendInfo {
private gitCommitHash = '';
private hostname = '';
private version = '';
private backendInfo: IBackendInfo;
constructor() {
this.setLatestCommitHash();
this.setVersion();
this.hostname = os.hostname();
}
public getBackendInfo(): IBackendInfo {
return {
hostname: this.hostname,
gitCommit: this.gitCommitHash,
version: this.version,
// This file is created by ./fetch-version.ts during building
const versionFile = path.join(__dirname, 'version.json')
var versionInfo;
if (fs.existsSync(versionFile)) {
versionInfo = JSON.parse(fs.readFileSync(versionFile).toString());
} else {
// Use dummy values if `versionFile` doesn't exist (e.g., during testing)
versionInfo = {
version: '?',
gitCommit: '?'
};
}
this.backendInfo = {
hostname: os.hostname(),
version: versionInfo.version,
gitCommit: versionInfo.gitCommit
};
}
public getBackendInfo(): IBackendInfo {
return this.backendInfo;
}
public getShortCommitHash() {
return this.gitCommitHash.slice(0, 7);
}
private setLatestCommitHash(): void {
//TODO: share this logic with `generate-config.js`
if (process.env.DOCKER_COMMIT_HASH) {
this.gitCommitHash = process.env.DOCKER_COMMIT_HASH;
} else {
try {
const gitRevParse = spawnSync('git', ['rev-parse', '--short', 'HEAD']);
if (!gitRevParse.error) {
const output = gitRevParse.stdout.toString('utf-8').replace(/[\n\r\s]+$/, '');
this.gitCommitHash = output ? output : '?';
} else if (gitRevParse.error.code === 'ENOENT') {
console.log('git not found, cannot parse git hash');
this.gitCommitHash = '?';
}
} catch (e: any) {
console.log('Could not load git commit info: ' + e.message);
this.gitCommitHash = '?';
}
}
}
private setVersion(): void {
try {
const packageJson = fs.readFileSync('package.json').toString();
this.version = JSON.parse(packageJson).version;
} catch (e) {
throw new Error(e instanceof Error ? e.message : 'Error');
}
return this.backendInfo.gitCommit.slice(0, 7);
}
}

View File

@@ -0,0 +1,381 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import { RequiredSpec } from '../../mempool.interfaces';
import bisq from './bisq';
import { MarketsApiError } from './interfaces';
import marketsApi from './markets-api';
class BisqRoutes {
public initRoutes(app: Application) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/stats', this.getBisqStats)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/tx/:txId', this.getBisqTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/block/:hash', this.getBisqBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/blocks/tip/height', this.getBisqTip)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/blocks/:index/:length', this.getBisqBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/address/:address', this.getBisqAddress)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/txs/:index/:length', this.getBisqTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/currencies', this.getBisqMarketCurrencies.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/depth', this.getBisqMarketDepth.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/hloc', this.getBisqMarketHloc.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/markets', this.getBisqMarketMarkets.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/offers', this.getBisqMarketOffers.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/ticker', this.getBisqMarketTicker.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/trades', this.getBisqMarketTrades.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/volumes', this.getBisqMarketVolumes.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/volumes/7d', this.getBisqMarketVolumes7d.bind(this))
;
}
private getBisqStats(req: Request, res: Response) {
const result = bisq.getStats();
res.json(result);
}
private getBisqTip(req: Request, res: Response) {
const result = bisq.getLatestBlockHeight();
res.type('text/plain');
res.send(result.toString());
}
private getBisqTransaction(req: Request, res: Response) {
const result = bisq.getTransaction(req.params.txId);
if (result) {
res.json(result);
} else {
res.status(404).send('Bisq transaction not found');
}
}
private getBisqTransactions(req: Request, res: Response) {
const types: string[] = [];
req.query.types = req.query.types || [];
if (!Array.isArray(req.query.types)) {
res.status(500).send('Types is not an array');
return;
}
for (const _type in req.query.types) {
if (typeof req.query.types[_type] === 'string') {
types.push(req.query.types[_type].toString());
}
}
const index = parseInt(req.params.index, 10) || 0;
const length = parseInt(req.params.length, 10) > 100 ? 100 : parseInt(req.params.length, 10) || 25;
const [transactions, count] = bisq.getTransactions(index, length, types);
res.header('X-Total-Count', count.toString());
res.json(transactions);
}
private getBisqBlock(req: Request, res: Response) {
const result = bisq.getBlock(req.params.hash);
if (result) {
res.json(result);
} else {
res.status(404).send('Bisq block not found');
}
}
private getBisqBlocks(req: Request, res: Response) {
const index = parseInt(req.params.index, 10) || 0;
const length = parseInt(req.params.length, 10) > 100 ? 100 : parseInt(req.params.length, 10) || 25;
const [transactions, count] = bisq.getBlocks(index, length);
res.header('X-Total-Count', count.toString());
res.json(transactions);
}
private getBisqAddress(req: Request, res: Response) {
const result = bisq.getAddress(req.params.address.substr(1));
if (result) {
res.json(result);
} else {
res.status(404).send('Bisq address not found');
}
}
private getBisqMarketCurrencies(req: Request, res: Response) {
const constraints: RequiredSpec = {
'type': {
required: false,
types: ['crypto', 'fiat', 'all']
},
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = marketsApi.getCurrencies(p.type);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketCurrencies error'));
}
}
private getBisqMarketDepth(req: Request, res: Response) {
const constraints: RequiredSpec = {
'market': {
required: true,
types: ['@string']
},
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = marketsApi.getDepth(p.market);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketDepth error'));
}
}
private getBisqMarketMarkets(req: Request, res: Response) {
const result = marketsApi.getMarkets();
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketMarkets error'));
}
}
private getBisqMarketTrades(req: Request, res: Response) {
const constraints: RequiredSpec = {
'market': {
required: true,
types: ['@string']
},
'timestamp_from': {
required: false,
types: ['@number']
},
'timestamp_to': {
required: false,
types: ['@number']
},
'trade_id_to': {
required: false,
types: ['@string']
},
'trade_id_from': {
required: false,
types: ['@string']
},
'direction': {
required: false,
types: ['buy', 'sell']
},
'limit': {
required: false,
types: ['@number']
},
'sort': {
required: false,
types: ['asc', 'desc']
}
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = marketsApi.getTrades(p.market, p.timestamp_from,
p.timestamp_to, p.trade_id_from, p.trade_id_to, p.direction, p.limit, p.sort);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketTrades error'));
}
}
private getBisqMarketOffers(req: Request, res: Response) {
const constraints: RequiredSpec = {
'market': {
required: true,
types: ['@string']
},
'direction': {
required: false,
types: ['buy', 'sell']
},
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = marketsApi.getOffers(p.market, p.direction);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketOffers error'));
}
}
private getBisqMarketVolumes(req: Request, res: Response) {
const constraints: RequiredSpec = {
'market': {
required: false,
types: ['@string']
},
'interval': {
required: false,
types: ['minute', 'half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto']
},
'timestamp_from': {
required: false,
types: ['@number']
},
'timestamp_to': {
required: false,
types: ['@number']
},
'milliseconds': {
required: false,
types: ['@boolean']
},
'timestamp': {
required: false,
types: ['no', 'yes']
},
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = marketsApi.getVolumes(p.market, p.timestamp_from, p.timestamp_to, p.interval, p.milliseconds, p.timestamp);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketVolumes error'));
}
}
private getBisqMarketHloc(req: Request, res: Response) {
const constraints: RequiredSpec = {
'market': {
required: true,
types: ['@string']
},
'interval': {
required: false,
types: ['minute', 'half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto']
},
'timestamp_from': {
required: false,
types: ['@number']
},
'timestamp_to': {
required: false,
types: ['@number']
},
'milliseconds': {
required: false,
types: ['@boolean']
},
'timestamp': {
required: false,
types: ['no', 'yes']
},
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = marketsApi.getHloc(p.market, p.interval, p.timestamp_from, p.timestamp_to, p.milliseconds, p.timestamp);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketHloc error'));
}
}
private getBisqMarketTicker(req: Request, res: Response) {
const constraints: RequiredSpec = {
'market': {
required: false,
types: ['@string']
},
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = marketsApi.getTicker(p.market);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketTicker error'));
}
}
private getBisqMarketVolumes7d(req: Request, res: Response) {
const result = marketsApi.getVolumesByTime(604800);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketVolumes7d error'));
}
}
private parseRequestParameters(requestParams: object, params: RequiredSpec): { [name: string]: any; } {
const final = {};
for (const i in params) {
if (params.hasOwnProperty(i)) {
if (params[i].required && requestParams[i] === undefined) {
return { error: i + ' parameter missing'};
}
if (typeof requestParams[i] === 'string') {
const str = (requestParams[i] || '').toString().toLowerCase();
if (params[i].types.indexOf('@number') > -1) {
const number = parseInt((str).toString(), 10);
final[i] = number;
} else if (params[i].types.indexOf('@string') > -1) {
final[i] = str;
} else if (params[i].types.indexOf('@boolean') > -1) {
final[i] = str === 'true' || str === 'yes';
} else if (params[i].types.indexOf(str) > -1) {
final[i] = str;
} else {
return { error: i + ' parameter invalid'};
}
} else if (typeof requestParams[i] === 'number') {
final[i] = requestParams[i];
}
}
}
return final;
}
private getBisqMarketErrorResponse(message: string): MarketsApiError {
return {
'success': 0,
'error': message
};
}
}
export default new BisqRoutes;

View File

@@ -4,15 +4,19 @@ export interface AbstractBitcoinApi {
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
$getBlockHeightTip(): Promise<number>;
$getBlockHashTip(): Promise<string>;
$getTxIdsForBlock(hash: string): Promise<string[]>;
$getBlockHash(height: number): Promise<string>;
$getBlockHeader(hash: string): Promise<string>;
$getBlock(hash: string): Promise<IEsploraApi.Block>;
$getRawBlock(hash: string): Promise<string>;
$getAddress(address: string): Promise<IEsploraApi.Address>;
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
$getAddressPrefix(prefix: string): string[];
$sendRawTransaction(rawTransaction: string): Promise<string>;
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
}
export interface BitcoinRpcCredentials {
host: string;

View File

@@ -73,6 +73,14 @@ export namespace IBitcoinApi {
time: number; // (numeric) Same as blocktime
}
export interface VerboseBlock extends Block {
tx: VerboseTransaction[]; // The transactions in the format of the getrawtransaction RPC. Different from verbosity = 1 "tx" result
}
export interface VerboseTransaction extends Transaction {
fee?: number; // (numeric) The transaction fee in BTC, omitted if block undo data is not available
}
export interface Vin {
txid?: string; // (string) The transaction id
vout?: number; // (string)

View File

@@ -64,13 +64,21 @@ class BitcoinApi implements AbstractBitcoinApi {
});
}
$getBlockHashTip(): Promise<string> {
return this.bitcoindClient.getChainTips()
.then((result: IBitcoinApi.ChainTips[]) => {
return result.find(tip => tip.status === 'active')!.hash;
});
}
$getTxIdsForBlock(hash: string): Promise<string[]> {
return this.bitcoindClient.getBlock(hash, 1)
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
}
$getRawBlock(hash: string): Promise<string> {
return this.bitcoindClient.getBlock(hash, 0);
return this.bitcoindClient.getBlock(hash, 0)
.then((raw: string) => Buffer.from(raw, "hex"));
}
$getBlockHash(height: number): Promise<string> {
@@ -123,6 +131,16 @@ class BitcoinApi implements AbstractBitcoinApi {
return this.bitcoindClient.sendRawTransaction(rawTransaction);
}
async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
return {
spent: txOut === null,
status: {
confirmed: true,
}
};
}
async $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
const outSpends: IEsploraApi.Outspend[] = [];
const tx = await this.$getRawTransaction(txId, true, false);
@@ -141,6 +159,15 @@ class BitcoinApi implements AbstractBitcoinApi {
return outSpends;
}
async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
const outspends: IEsploraApi.Outspend[][] = [];
for (const tx of txId) {
const outspend = await this.$getOutspends(tx);
outspends.push(outspend);
}
return outspends;
}
$getEstimatedHashrate(blockHeight: number): Promise<number> {
// 120 is the default block span in Core
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
@@ -179,7 +206,9 @@ class BitcoinApi implements AbstractBitcoinApi {
sequence: vin.sequence,
txid: vin.txid || '',
vout: vin.vout || 0,
witness: vin.txinwitness,
witness: vin.txinwitness || [],
inner_redeemscript_asm: '',
inner_witnessscript_asm: '',
};
});

View File

@@ -0,0 +1,559 @@
import { Application, Request, Response } from 'express';
import axios from 'axios';
import config from '../../config';
import websocketHandler from '../websocket-handler';
import mempool from '../mempool';
import feeApi from '../fee-api';
import mempoolBlocks from '../mempool-blocks';
import bitcoinApi from './bitcoin-api-factory';
import { Common } from '../common';
import backendInfo from '../backend-info';
import transactionUtils from '../transaction-utils';
import { IEsploraApi } from './esplora-api.interface';
import loadingIndicators from '../loading-indicators';
import { TransactionExtended } from '../../mempool.interfaces';
import logger from '../../logger';
import blocks from '../blocks';
import bitcoinClient from './bitcoin-client';
import difficultyAdjustment from '../difficulty-adjustment';
class BitcoinRoutes {
public initRoutes(app: Application) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', this.getTransactionTimes)
.get(config.MEMPOOL.API_URL_PREFIX + 'outspends', this.$getBatchedOutspends)
.get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', this.getCpfpInfo)
.get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', this.getDifficultyChange)
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', this.getRecommendedFees)
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', this.getMempoolBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', this.getBackendInfo)
.get(config.MEMPOOL.API_URL_PREFIX + 'init-data', this.getInitData)
.get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', this.validateAddress)
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/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(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/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(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/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(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', this.getBlocks.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions);
;
if (config.MEMPOOL.BACKEND !== 'esplora') {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool', this.getMempool)
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool/txids', this.getMempoolTxIds)
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', this.getRecentMempoolTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', this.getTransaction)
.post(config.MEMPOOL.API_URL_PREFIX + 'tx', this.$postTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', this.getBlockHeader)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', this.getBlockTipHash)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/raw', this.getRawBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', this.getTxIdsForBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs', this.getBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs/:index', this.getBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', this.getBlockHeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', this.getAddress)
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', this.getAddressTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs/chain/:txId', this.getAddressTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', this.getAddressPrefix)
;
}
}
private getInitData(req: Request, res: Response) {
try {
const result = websocketHandler.getInitData();
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private getRecommendedFees(req: Request, res: Response) {
if (!mempool.isInSync()) {
res.statusCode = 503;
res.send('Service Unavailable');
return;
}
const result = feeApi.getRecommendedFee();
res.json(result);
}
private getMempoolBlocks(req: Request, res: Response) {
try {
const result = mempoolBlocks.getMempoolBlocks();
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private getTransactionTimes(req: Request, res: Response) {
if (!Array.isArray(req.query.txId)) {
res.status(500).send('Not an array');
return;
}
const txIds: string[] = [];
for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') {
txIds.push(req.query.txId[_txId].toString());
}
}
const times = mempool.getFirstSeenForTransactions(txIds);
res.json(times);
}
private async $getBatchedOutspends(req: Request, res: Response) {
if (!Array.isArray(req.query.txId)) {
res.status(500).send('Not an array');
return;
}
if (req.query.txId.length > 50) {
res.status(400).send('Too many txids requested');
return;
}
const txIds: string[] = [];
for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') {
txIds.push(req.query.txId[_txId].toString());
}
}
try {
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txIds);
res.json(batchedOutspends);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private 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);
}
private getBackendInfo(req: Request, res: Response) {
res.json(backendInfo.getBackendInfo());
}
private async getTransaction(req: Request, res: Response) {
try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
res.json(transaction);
} catch (e) {
let statusCode = 500;
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
}
res.status(statusCode).send(e instanceof Error ? e.message : e);
}
}
private async getRawTransaction(req: Request, res: Response) {
try {
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true);
res.setHeader('content-type', 'text/plain');
res.send(transaction.hex);
} catch (e) {
let statusCode = 500;
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
}
res.status(statusCode).send(e instanceof Error ? e.message : e);
}
}
private 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 instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
}
res.status(statusCode).send(e instanceof Error ? e.message : e);
}
}
private async getBlock(req: Request, res: Response) {
try {
const block = await blocks.$getBlock(req.params.hash);
const blockAge = new Date().getTime() / 1000 - block.timestamp;
const day = 24 * 3600;
let cacheDuration;
if (blockAge > 365 * day) {
cacheDuration = 30 * day;
} else if (blockAge > 30 * day) {
cacheDuration = 10 * day;
} else {
cacheDuration = 600
}
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
res.json(block);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlockHeader(req: Request, res: Response) {
try {
const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash);
res.setHeader('content-type', 'text/plain');
res.send(blockHeader);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getStrippedBlockTransactions(req: Request, res: Response) {
try {
const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transactions);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlocks(req: Request, res: Response) {
try {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await blocks.$getBlocks(height, 15));
} else { // Liquid, Bisq
return await this.getLegacyBlocks(req, res);
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getLegacyBlocks(req: Request, res: Response) {
try {
const returnBlocks: IEsploraApi.Block[] = [];
const fromHeight = parseInt(req.params.height, 10) || blocks.getCurrentBlockHeight();
// Check if block height exist in local cache to skip the hash lookup
const blockByHeight = blocks.getBlocks().find((b) => b.height === fromHeight);
let startFromHash: string | null = null;
if (blockByHeight) {
startFromHash = blockByHeight.id;
} else {
startFromHash = await bitcoinApi.$getBlockHash(fromHeight);
}
let nextHash = startFromHash;
for (let i = 0; i < 10 && nextHash; i++) {
const localBlock = blocks.getBlocks().find((b) => b.id === nextHash);
if (localBlock) {
returnBlocks.push(localBlock);
nextHash = localBlock.previousblockhash;
} else {
const block = await bitcoinApi.$getBlock(nextHash);
returnBlocks.push(block);
nextHash = block.previousblockhash;
}
}
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(returnBlocks);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlockTransactions(req: Request, res: Response) {
try {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
const txIds = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
const transactions: TransactionExtended[] = [];
const startingIndex = Math.max(0, parseInt(req.params.index || '0', 10));
const endIndex = Math.min(startingIndex + 10, txIds.length);
for (let i = startingIndex; i < endIndex; i++) {
try {
const transaction = await transactionUtils.$getTransactionExtended(txIds[i], true, true);
transactions.push(transaction);
loadingIndicators.setProgress('blocktxs-' + req.params.hash, (i - startingIndex + 1) / (endIndex - startingIndex) * 100);
} catch (e) {
logger.debug('getBlockTransactions error: ' + (e instanceof Error ? e.message : e));
}
}
res.json(transactions);
} catch (e) {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlockHeight(req: Request, res: Response) {
try {
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
res.send(blockHash);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getAddress(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
try {
const addressData = await bitcoinApi.$getAddress(req.params.address);
res.json(addressData);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
return res.status(413).send(e instanceof Error ? e.message : e);
}
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getAddressTransactions(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
try {
const transactions = await bitcoinApi.$getAddressTransactions(req.params.address, req.params.txId);
res.json(transactions);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
return res.status(413).send(e instanceof Error ? e.message : e);
}
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getAdressTxChain(req: Request, res: Response) {
res.status(501).send('Not implemented');
}
private async getAddressPrefix(req: Request, res: Response) {
try {
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
res.send(blockHash);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private 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);
}
private async getMempool(req: Request, res: Response) {
const info = mempool.getMempoolInfo();
res.json({
count: info.size,
vsize: info.bytes,
total_fee: info.total_fee * 1e8,
fee_histogram: []
});
}
private async getMempoolTxIds(req: Request, res: Response) {
try {
const rawMempool = await bitcoinApi.$getRawMempool();
res.send(rawMempool);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlockTipHeight(req: Request, res: Response) {
try {
const result = await bitcoinApi.$getBlockHeightTip();
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlockTipHash(req: Request, res: Response) {
try {
const result = await bitcoinApi.$getBlockHashTip();
res.setHeader('content-type', 'text/plain');
res.send(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getRawBlock(req: Request, res: Response) {
try {
const result = await bitcoinApi.$getRawBlock(req.params.hash);
res.setHeader('content-type', 'application/octet-stream');
res.send(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private 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 instanceof Error ? e.message : e);
}
}
private async validateAddress(req: Request, res: Response) {
try {
const result = await bitcoinClient.validateAddress(req.params.address);
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getTransactionOutspends(req: Request, res: Response) {
try {
const result = await bitcoinApi.$getOutspends(req.params.txId);
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private getDifficultyChange(req: Request, res: Response) {
try {
const da = difficultyAdjustment.getDifficultyAdjustment();
if (da) {
res.json(da);
} else {
res.status(503).send(`Service Temporarily Unavailable`);
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $postTransaction(req: Request, res: Response) {
res.setHeader('content-type', 'text/plain');
try {
let rawTx;
if (typeof req.body === 'object') {
rawTx = Object.keys(req.body)[0];
} else {
rawTx = req.body;
}
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
res.send(txIdResult);
} catch (e: any) {
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
private async $postTransactionForm(req: Request, res: Response) {
res.setHeader('content-type', 'text/plain');
const matches = /tx=([a-z0-9]+)/.exec(req.body);
let txHex = '';
if (matches && matches[1]) {
txHex = matches[1];
}
try {
const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
res.send(txIdResult);
} catch (e: any) {
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
}
export default new BitcoinRoutes();

View File

@@ -1,12 +1,11 @@
import config from '../../config';
import Client from '@mempool/electrum-client';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface';
import { IElectrumApi } from './electrum-api.interface';
import BitcoinApi from './bitcoin-api';
import logger from '../../logger';
import * as ElectrumClient from '@mempool/electrum-client';
import * as sha256 from 'crypto-js/sha256';
import * as hexEnc from 'crypto-js/enc-hex';
import crypto from "crypto-js";
import loadingIndicators from '../loading-indicators';
import memoryCache from '../memory-cache';
@@ -26,7 +25,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
onLog: (str) => { logger.debug(str); },
};
this.electrumClient = new ElectrumClient(
this.electrumClient = new Client(
config.ELECTRUM.PORT,
config.ELECTRUM.HOST,
config.ELECTRUM.TLS_ENABLED ? 'tls' : 'tcp',
@@ -35,7 +34,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
);
this.electrumClient.initElectrum(electrumConfig, electrumPersistencePolicy)
.then(() => {})
.then(() => { })
.catch((err) => {
logger.err(`Error connecting to Electrum Server at ${config.ELECTRUM.HOST}:${config.ELECTRUM.PORT}`);
});
@@ -95,7 +94,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
async $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
const addressInfo = await this.bitcoindClient.validateAddress(address);
if (!addressInfo || !addressInfo.isvalid) {
return [];
return [];
}
try {
@@ -144,8 +143,8 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
}
private encodeScriptHash(scriptPubKey: string): string {
const addrScripthash = hexEnc.stringify(sha256(hexEnc.parse(scriptPubKey)));
return addrScripthash.match(/.{2}/g).reverse().join('');
const addrScripthash = crypto.enc.Hex.stringify(crypto.SHA256(crypto.enc.Hex.parse(scriptPubKey)));
return addrScripthash!.match(/.{2}/g)!.reverse().join('');
}
}

View File

@@ -25,10 +25,10 @@ export namespace IEsploraApi {
is_coinbase: boolean;
scriptsig: string;
scriptsig_asm: string;
inner_redeemscript_asm?: string;
inner_witnessscript_asm?: string;
inner_redeemscript_asm: string;
inner_witnessscript_asm: string;
sequence: any;
witness?: string[];
witness: string[];
prevout: Vout | null;
// Elements
is_pegin?: boolean;

View File

@@ -25,6 +25,11 @@ class ElectrsApi implements AbstractBitcoinApi {
.then((response) => response.data);
}
$getBlockHashTip(): Promise<string> {
return axios.get<string>(config.ESPLORA.REST_API_URL + '/blocks/tip/hash', this.axiosConfig)
.then((response) => response.data);
}
$getTxIdsForBlock(hash: string): Promise<string[]> {
return axios.get<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig)
.then((response) => response.data);
@@ -45,6 +50,11 @@ class ElectrsApi implements AbstractBitcoinApi {
.then((response) => response.data);
}
$getRawBlock(hash: string): Promise<string> {
return axios.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", this.axiosConfig)
.then((response) => response.data);
}
$getAddress(address: string): Promise<IEsploraApi.Address> {
throw new Error('Method getAddress not implemented.');
}
@@ -61,8 +71,23 @@ class ElectrsApi implements AbstractBitcoinApi {
throw new Error('Method not implemented.');
}
$getOutspends(): Promise<IEsploraApi.Outspend[]> {
throw new Error('Method not implemented.');
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
return axios.get<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout, this.axiosConfig)
.then((response) => response.data);
}
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
return axios.get<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig)
.then((response) => response.data);
}
async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
const outspends: IEsploraApi.Outspend[][] = [];
for (const tx of txId) {
const outspend = await this.$getOutspends(tx);
outspends.push(outspend);
}
return outspends;
}
}

View File

@@ -2,11 +2,12 @@ import config from '../config';
import bitcoinApi from './bitcoin/bitcoin-api-factory';
import logger from '../logger';
import memPool from './mempool';
import { BlockExtended, PoolTag, TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
import { BlockExtended, BlockSummary, PoolTag, TransactionExtended, TransactionStripped, TransactionMinerInfo } from '../mempool.interfaces';
import { Common } from './common';
import diskCache from './disk-cache';
import transactionUtils from './transaction-utils';
import bitcoinClient from './bitcoin/bitcoin-client';
import { IBitcoinApi } from './bitcoin/bitcoin-api.interface';
import { IEsploraApi } from './bitcoin/esplora-api.interface';
import poolsRepository from '../repositories/PoolsRepository';
import blocksRepository from '../repositories/BlocksRepository';
@@ -17,11 +18,16 @@ import BlocksRepository from '../repositories/BlocksRepository';
import HashratesRepository from '../repositories/HashratesRepository';
import indexer from '../indexer';
import fiatConversion from './fiat-conversion';
import RatesRepository from '../repositories/RatesRepository';
import poolsParser from './pools-parser';
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
import mining from './mining/mining';
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
import PricesRepository from '../repositories/PricesRepository';
import priceUpdater from '../tasks/price-updater';
class Blocks {
private blocks: BlockExtended[] = [];
private blockSummaries: BlockSummary[] = [];
private currentBlockHeight = 0;
private currentDifficulty = 0;
private lastDifficultyAdjustmentTime = 0;
@@ -38,6 +44,14 @@ class Blocks {
this.blocks = blocks;
}
public getBlockSummaries(): BlockSummary[] {
return this.blockSummaries;
}
public setBlockSummaries(blockSummaries: BlockSummary[]) {
this.blockSummaries = blockSummaries;
}
public setNewBlockCallback(fn: (block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void) {
this.newBlockCallbacks.push(fn);
}
@@ -106,6 +120,27 @@ class Blocks {
return transactions;
}
/**
* Return a block summary (list of stripped transactions)
* @param block
* @returns BlockSummary
*/
private summarizeBlock(block: IBitcoinApi.VerboseBlock): BlockSummary {
const stripped = block.tx.map((tx) => {
return {
txid: tx.txid,
vsize: tx.vsize,
fee: tx.fee ? Math.round(tx.fee * 100000000) : 0,
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0) * 100000000)
};
});
return {
id: block.hash,
transactions: stripped
};
}
/**
* Return a block with additional data (reward, coinbase, fees...)
* @param block
@@ -117,6 +152,7 @@ class Blocks {
blockExtended.extras.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0);
blockExtended.extras.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]);
blockExtended.extras.coinbaseRaw = blockExtended.extras.coinbaseTx.vin[0].scriptsig;
blockExtended.extras.usd = fiatConversion.getConversionRates().USD;
if (block.height === 0) {
blockExtended.extras.medianFee = 0; // 50th percentiles
@@ -135,7 +171,7 @@ class Blocks {
blockExtended.extras.avgFeeRate = stats.avgfeerate;
}
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
let pool: PoolTag;
if (blockExtended.extras?.coinbaseTx !== undefined) {
pool = await this.$findBlockMiner(blockExtended.extras?.coinbaseTx);
@@ -212,15 +248,70 @@ class Blocks {
}
/**
* [INDEXING] Index all blocks metadata for the mining dashboard
* [INDEXING] Index all blocks summaries for the block txs visualization
*/
public async $generateBlockDatabase() {
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
if (blockchainInfo.blocks !== blockchainInfo.headers) { // Wait for node to sync
public async $generateBlocksSummariesDatabase() {
if (Common.blocksSummariesIndexingEnabled() === false) {
return;
}
try {
// Get all indexed block hash
const indexedBlocks = await blocksRepository.$getIndexedBlocks();
const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId();
const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop
for (const hash of indexedBlockSummariesHashesArray) {
indexedBlockSummariesHashes[hash] = true;
}
// Logging
let newlyIndexed = 0;
let totalIndexed = indexedBlockSummariesHashesArray.length;
let indexedThisRun = 0;
let timer = new Date().getTime() / 1000;
const startedAt = new Date().getTime() / 1000;
for (const block of indexedBlocks) {
if (indexedBlockSummariesHashes[block.hash] === true) {
continue;
}
// Logging
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
if (elapsedSeconds > 5) {
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100;
logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`);
timer = new Date().getTime() / 1000;
indexedThisRun = 0;
}
await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary
// Logging
indexedThisRun++;
totalIndexed++;
newlyIndexed++;
}
if (newlyIndexed > 0) {
logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`);
} else {
logger.debug(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`);
}
} catch (e) {
logger.err(`Blocks summaries indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
throw e;
}
}
/**
* [INDEXING] Index all blocks metadata for the mining dashboard
*/
public async $generateBlockDatabase(): Promise<boolean> {
try {
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
let currentBlockHeight = blockchainInfo.blocks;
let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, blockchainInfo.blocks);
@@ -261,10 +352,9 @@ class Blocks {
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const blockPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100;
const timeLeft = Math.round((indexingBlockAmount - totalIndexed) / blockPerSeconds);
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`);
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds`);
timer = new Date().getTime() / 1000;
indexedThisRun = 0;
loadingIndicators.setProgress('block-indexing', progress, false);
@@ -280,18 +370,19 @@ class Blocks {
currentBlockHeight -= chunkSize;
}
logger.info(`Indexed ${newlyIndexed} blocks`);
if (newlyIndexed > 0) {
logger.notice(`Block indexing completed: indexed ${newlyIndexed} blocks`);
} else {
logger.debug(`Block indexing completed: indexed ${newlyIndexed} blocks`);
}
loadingIndicators.setProgress('block-indexing', 100);
} catch (e) {
logger.err('Block indexing failed. Trying again later. Reason: ' + (e instanceof Error ? e.message : e));
logger.err('Block indexing failed. Trying again in 10 seconds. Reason: ' + (e instanceof Error ? e.message : e));
loadingIndicators.setProgress('block-indexing', 100);
return;
throw e;
}
const chainValid = await BlocksRepository.$validateChain();
if (!chainValid) {
indexer.reindex();
}
return await BlocksRepository.$validateChain();
}
public async $updateBlocks() {
@@ -323,7 +414,7 @@ class Blocks {
if (blockHeightTip >= 2016) {
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
const previousPeriodBlock = await bitcoinApi.$getBlock(previousPeriodBlockHash);
const previousPeriodBlock = await bitcoinClient.getBlock(previousPeriodBlockHash)
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
logger.debug(`Initial difficulty adjustment data set.`);
}
@@ -341,10 +432,12 @@ class Blocks {
}
const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight);
const block = BitcoinApi.convertBlock(await bitcoinClient.getBlock(blockHash));
const verboseBlock = await bitcoinClient.getBlock(blockHash, 2);
const block = BitcoinApi.convertBlock(verboseBlock);
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false);
const blockExtended: BlockExtended = await this.$getBlockExtended(block, transactions);
const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock);
if (Common.indexingEnabled()) {
if (!fastForwarded) {
@@ -354,18 +447,48 @@ class Blocks {
// We assume there won't be a reorg with more than 10 block depth
await BlocksRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
await HashratesRepository.$deleteLastEntries();
await BlocksSummariesRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
for (let i = 10; i >= 0; --i) {
await this.$indexBlock(lastBlock['height'] - i);
const newBlock = await this.$indexBlock(lastBlock['height'] - i);
await this.$getStrippedBlockTransactions(newBlock.id, true, true);
}
await mining.$indexDifficultyAdjustments();
await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`);
indexer.reindex();
}
await blocksRepository.$saveBlockInDatabase(blockExtended);
const lastestPriceId = await PricesRepository.$getLatestPriceId();
if (priceUpdater.historyInserted === true && lastestPriceId !== null) {
await blocksRepository.$saveBlockPrices([{
height: blockExtended.height,
priceId: lastestPriceId,
}]);
} else {
logger.info(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`)
setTimeout(() => {
indexer.runSingleTask('blocksPrices');
}, 10000);
}
// Save blocks summary for visualization if it's enabled
if (Common.blocksSummariesIndexingEnabled() === true) {
await this.$getStrippedBlockTransactions(blockExtended.id, true);
}
}
}
if (fiatConversion.ratesInitialized === true && config.DATABASE.ENABLED === true) {
await RatesRepository.$saveRate(blockExtended.height, fiatConversion.getConversionRates());
}
if (block.height % 2016 === 0) {
if (Common.indexingEnabled()) {
await DifficultyAdjustmentsRepository.$saveAdjustments({
time: block.timestamp,
height: block.height,
difficulty: block.difficulty,
adjustment: Math.round((block.difficulty / this.currentDifficulty) * 1000000) / 1000000, // Remove float point noise
});
}
this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100;
this.lastDifficultyAdjustmentTime = block.timestamp;
this.currentDifficulty = block.difficulty;
@@ -375,6 +498,10 @@ class Blocks {
if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
this.blocks = this.blocks.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
}
this.blockSummaries.push(blockSummary);
if (this.blockSummaries.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
this.blockSummaries = this.blockSummaries.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
}
if (this.newBlockCallbacks.length) {
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
@@ -422,13 +549,14 @@ class Blocks {
}
}
const block = await bitcoinApi.$getBlock(hash);
// Not Bitcoin network, return the block as it
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
return block;
return await bitcoinApi.$getBlock(hash);
}
let block = await bitcoinClient.getBlock(hash);
block = prepareBlock(block);
// Bitcoin network, add our custom data on top
const transactions = await this.$getTransactionsExtended(hash, block.height, true);
const blockExtended = await this.$getBlockExtended(block, transactions);
@@ -440,48 +568,71 @@ class Blocks {
return blockExtended;
}
public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> {
try {
let currentHeight = fromHeight !== undefined ? fromHeight : this.getCurrentBlockHeight();
const returnBlocks: BlockExtended[] = [];
if (currentHeight < 0) {
return returnBlocks;
public async $getStrippedBlockTransactions(hash: string, skipMemoryCache = false,
skipDBLookup = false): Promise<TransactionStripped[]>
{
if (skipMemoryCache === false) {
// Check the memory cache
const cachedSummary = this.getBlockSummaries().find((b) => b.id === hash);
if (cachedSummary) {
return cachedSummary.transactions;
}
if (currentHeight === 0 && Common.indexingEnabled()) {
currentHeight = await blocksRepository.$mostRecentBlockHeight();
}
// Check if block height exist in local cache to skip the hash lookup
const blockByHeight = this.getBlocks().find((b) => b.height === currentHeight);
let startFromHash: string | null = null;
if (blockByHeight) {
startFromHash = blockByHeight.id;
} else if (!Common.indexingEnabled()) {
startFromHash = await bitcoinApi.$getBlockHash(currentHeight);
}
let nextHash = startFromHash;
for (let i = 0; i < limit && currentHeight >= 0; i++) {
let block = this.getBlocks().find((b) => b.height === currentHeight);
if (block) {
returnBlocks.push(block);
} else if (Common.indexingEnabled()) {
block = await this.$indexBlock(currentHeight);
returnBlocks.push(block);
} else if (nextHash != null) {
block = prepareBlock(await bitcoinApi.$getBlock(nextHash));
nextHash = block.previousblockhash;
returnBlocks.push(block);
}
currentHeight--;
}
return returnBlocks;
} catch (e) {
throw e;
}
// Check if it's indexed in db
if (skipDBLookup === false && Common.blocksSummariesIndexingEnabled() === true) {
const indexedSummary = await BlocksSummariesRepository.$getByBlockId(hash);
if (indexedSummary !== undefined) {
return indexedSummary.transactions;
}
}
// Call Core RPC
const block = await bitcoinClient.getBlock(hash, 2);
const summary = this.summarizeBlock(block);
// Index the response if needed
if (Common.blocksSummariesIndexingEnabled() === true) {
await BlocksSummariesRepository.$saveSummary({height: block.height, mined: summary});
}
return summary.transactions;
}
public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> {
let currentHeight = fromHeight !== undefined ? fromHeight : await blocksRepository.$mostRecentBlockHeight();
const returnBlocks: BlockExtended[] = [];
if (currentHeight < 0) {
return returnBlocks;
}
// Check if block height exist in local cache to skip the hash lookup
const blockByHeight = this.getBlocks().find((b) => b.height === currentHeight);
let startFromHash: string | null = null;
if (blockByHeight) {
startFromHash = blockByHeight.id;
} else if (!Common.indexingEnabled()) {
startFromHash = await bitcoinApi.$getBlockHash(currentHeight);
}
let nextHash = startFromHash;
for (let i = 0; i < limit && currentHeight >= 0; i++) {
let block = this.getBlocks().find((b) => b.height === currentHeight);
if (block) {
returnBlocks.push(block);
} else if (Common.indexingEnabled()) {
block = await this.$indexBlock(currentHeight);
returnBlocks.push(block);
} else if (nextHash != null) {
block = prepareBlock(await bitcoinClient.getBlock(nextHash));
nextHash = block.previousblockhash;
returnBlocks.push(block);
}
currentHeight--;
}
return returnBlocks;
}
public getLastDifficultyAdjustmentTime(): number {

View File

@@ -1,5 +1,7 @@
import { CpfpInfo, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
import config from '../config';
import { NodeSocket } from '../repositories/NodesSocketsRepository';
import { isIP } from 'net';
export class Common {
static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ?
'144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49'
@@ -114,7 +116,7 @@ export class Common {
totalFees += tx.bestDescendant.fee;
}
tx.effectiveFeePerVsize = Math.max(Common.isLiquid() ? 0.1 : 1, totalFees / (totalWeight / 4));
tx.effectiveFeePerVsize = Math.max(0, totalFees / (totalWeight / 4));
tx.cpfpChecked = true;
return {
@@ -172,9 +174,129 @@ export class Common {
static indexingEnabled(): boolean {
return (
['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK) &&
['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) &&
config.DATABASE.ENABLED === true &&
config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== 0
);
}
static blocksSummariesIndexingEnabled(): boolean {
return (
Common.indexingEnabled() &&
config.MEMPOOL.BLOCKS_SUMMARIES_INDEXING === true
);
}
static setDateMidnight(date: Date): void {
date.setUTCHours(0);
date.setUTCMinutes(0);
date.setUTCSeconds(0);
date.setUTCMilliseconds(0);
}
static channelShortIdToIntegerId(channelId: string): string {
if (channelId.indexOf('x') === -1) { // Already an integer id
return channelId;
}
if (channelId.indexOf('/') !== -1) { // Topology import
channelId = channelId.slice(0, -2);
}
const s = channelId.split('x').map(part => BigInt(part));
return ((s[0] << 40n) | (s[1] << 16n) | s[2]).toString();
}
/** Decodes a channel id returned by lnd as uint64 to a short channel id */
static channelIntegerIdToShortId(id: string): string {
if (id.indexOf('/') !== -1) {
id = id.slice(0, -2);
}
if (id.indexOf('x') !== -1) { // Already a short id
return id;
}
const n = BigInt(id);
return [
n >> 40n, // nth block
(n >> 16n) & 0xffffffn, // nth tx of the block
n & 0xffffn // nth output of the tx
].join('x');
}
static utcDateToMysql(date?: number): string {
const d = new Date((date || 0) * 1000);
return d.toISOString().split('T')[0] + ' ' + d.toTimeString().split(' ')[0];
}
static findSocketNetwork(addr: string): {network: string | null, url: string} {
let network: string | null = null;
let url = addr.split('://')[1];
if (!url) {
return {
network: null,
url: addr,
};
}
if (addr.indexOf('onion') !== -1) {
if (url.split('.')[0].length >= 56) {
network = 'torv3';
} else {
network = 'torv2';
}
} else if (addr.indexOf('i2p') !== -1) {
network = 'i2p';
} else if (addr.indexOf('ipv4') !== -1) {
const ipv = isIP(url.split(':')[0]);
if (ipv === 4) {
network = 'ipv4';
} else {
return {
network: null,
url: addr,
};
}
} else if (addr.indexOf('ipv6') !== -1) {
url = url.split('[')[1].split(']')[0];
const ipv = isIP(url);
if (ipv === 6) {
const parts = addr.split(':');
network = 'ipv6';
url = `[${url}]:${parts[parts.length - 1]}`;
} else {
return {
network: null,
url: addr,
};
}
} else {
return {
network: null,
url: addr,
};
}
return {
network: network,
url: url,
};
}
static formatSocket(publicKey: string, socket: {network: string, addr: string}): NodeSocket {
if (config.LIGHTNING.BACKEND === 'cln') {
return {
publicKey: publicKey,
network: socket.network,
addr: socket.addr,
};
} else /* if (config.LIGHTNING.BACKEND === 'lnd') */ {
const formatted = this.findSocketNetwork(socket.addr);
return {
publicKey: publicKey,
network: formatted.network,
addr: formatted.url,
};
}
}
}

View File

@@ -4,11 +4,25 @@ import logger from '../logger';
import { Common } from './common';
class DatabaseMigration {
private static currentVersion = 19;
private static currentVersion = 40;
private queryTimeout = 120000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
private blocksTruncatedMessage = `'blocks' table has been truncated.`;
private hashratesTruncatedMessage = `'hashrates' table has been truncated.`;
/**
* Avoid printing multiple time the same message
*/
private uniqueLog(loggerFunction: any, msg: string) {
if (this.uniqueLogs.includes(msg)) {
return;
}
this.uniqueLogs.push(msg);
loggerFunction(msg);
}
constructor() { }
/**
* Entry point
*/
@@ -39,6 +53,16 @@ class DatabaseMigration {
process.exit(-1);
}
if (databaseSchemaVersion === 0) {
logger.info('Initializing database (first run, clean install)');
}
if (databaseSchemaVersion <= 2) {
// Disable some spam logs when they're not relevant
this.uniqueLogs.push(this.blocksTruncatedMessage);
this.uniqueLogs.push(this.hashratesTruncatedMessage);
}
logger.debug('MIGRATIONS: Current state.schema_version ' + databaseSchemaVersion);
logger.debug('MIGRATIONS: Latest DatabaseMigration.version is ' + DatabaseMigration.currentVersion);
if (databaseSchemaVersion >= DatabaseMigration.currentVersion) {
@@ -56,10 +80,13 @@ class DatabaseMigration {
}
if (DatabaseMigration.currentVersion > databaseSchemaVersion) {
logger.notice('MIGRATIONS: Upgrading database schema');
try {
await this.$migrateTableSchemaFromVersion(databaseSchemaVersion);
logger.notice(`MIGRATIONS: OK. Database schema have been migrated from version ${databaseSchemaVersion} to ${DatabaseMigration.currentVersion} (latest version)`);
if (databaseSchemaVersion === 0) {
logger.notice(`MIGRATIONS: OK. Database schema has been properly initialized to version ${DatabaseMigration.currentVersion} (latest version)`);
} else {
logger.notice(`MIGRATIONS: OK. Database schema have been migrated from version ${databaseSchemaVersion} to ${DatabaseMigration.currentVersion} (latest version)`);
}
} catch (e) {
logger.err('MIGRATIONS: Unable to migrate database, aborting. ' + e);
}
@@ -75,121 +102,251 @@ class DatabaseMigration {
await this.$setStatisticsAddedIndexedFlag(databaseSchemaVersion);
const isBitcoin = ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK);
try {
await this.$executeQuery(this.getCreateElementsTableQuery(), await this.$checkIfTableExists('elements_pegs'));
await this.$executeQuery(this.getCreateStatisticsQuery(), await this.$checkIfTableExists('statistics'));
if (databaseSchemaVersion < 2 && this.statisticsAddedIndexed === false) {
await this.$executeQuery(`CREATE INDEX added ON statistics (added);`);
}
if (databaseSchemaVersion < 3) {
await this.$executeQuery(this.getCreatePoolsTableQuery(), await this.$checkIfTableExists('pools'));
}
if (databaseSchemaVersion < 4) {
await this.$executeQuery('DROP table IF EXISTS blocks;');
await this.$executeQuery(this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks'));
}
if (databaseSchemaVersion < 5 && isBitcoin === true) {
logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.`);
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
}
if (databaseSchemaVersion < 6 && isBitcoin === true) {
logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.`);
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
// Cleanup original blocks fields type
await this.$executeQuery('ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `tx_count` smallint unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `size` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `weight` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` double NOT NULL DEFAULT "0"');
// We also fix the pools.id type so we need to drop/re-create the foreign key
await this.$executeQuery('ALTER TABLE blocks DROP FOREIGN KEY IF EXISTS `blocks_ibfk_1`');
await this.$executeQuery('ALTER TABLE pools MODIFY `id` smallint unsigned AUTO_INCREMENT');
await this.$executeQuery('ALTER TABLE blocks MODIFY `pool_id` smallint unsigned NULL');
await this.$executeQuery('ALTER TABLE blocks ADD FOREIGN KEY (`pool_id`) REFERENCES `pools` (`id`)');
// Add new block indexing fields
await this.$executeQuery('ALTER TABLE blocks ADD `version` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `bits` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""');
await this.$executeQuery('ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL');
}
await this.$executeQuery(this.getCreateElementsTableQuery(), await this.$checkIfTableExists('elements_pegs'));
await this.$executeQuery(this.getCreateStatisticsQuery(), await this.$checkIfTableExists('statistics'));
if (databaseSchemaVersion < 2 && this.statisticsAddedIndexed === false) {
await this.$executeQuery(`CREATE INDEX added ON statistics (added);`);
}
if (databaseSchemaVersion < 3) {
await this.$executeQuery(this.getCreatePoolsTableQuery(), await this.$checkIfTableExists('pools'));
}
if (databaseSchemaVersion < 4) {
await this.$executeQuery('DROP table IF EXISTS blocks;');
await this.$executeQuery(this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks'));
}
if (databaseSchemaVersion < 5 && isBitcoin === true) {
this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
}
if (databaseSchemaVersion < 7 && isBitcoin === true) {
await this.$executeQuery('DROP table IF EXISTS hashrates;');
await this.$executeQuery(this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates'));
}
if (databaseSchemaVersion < 6 && isBitcoin === true) {
this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
// Cleanup original blocks fields type
await this.$executeQuery('ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `tx_count` smallint unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `size` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `weight` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` double NOT NULL DEFAULT "0"');
// We also fix the pools.id type so we need to drop/re-create the foreign key
await this.$executeQuery('ALTER TABLE blocks DROP FOREIGN KEY IF EXISTS `blocks_ibfk_1`');
await this.$executeQuery('ALTER TABLE pools MODIFY `id` smallint unsigned AUTO_INCREMENT');
await this.$executeQuery('ALTER TABLE blocks MODIFY `pool_id` smallint unsigned NULL');
await this.$executeQuery('ALTER TABLE blocks ADD FOREIGN KEY (`pool_id`) REFERENCES `pools` (`id`)');
// Add new block indexing fields
await this.$executeQuery('ALTER TABLE blocks ADD `version` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `bits` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""');
await this.$executeQuery('ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL');
}
if (databaseSchemaVersion < 8 && isBitcoin === true) {
logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.`);
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
await this.$executeQuery('ALTER TABLE `hashrates` DROP INDEX `PRIMARY`');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
}
if (databaseSchemaVersion < 7 && isBitcoin === true) {
await this.$executeQuery('DROP table IF EXISTS hashrates;');
await this.$executeQuery(this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates'));
}
if (databaseSchemaVersion < 9 && isBitcoin === true) {
logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.`);
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)');
await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)');
}
if (databaseSchemaVersion < 8 && isBitcoin === true) {
this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
await this.$executeQuery('ALTER TABLE `hashrates` DROP INDEX `PRIMARY`');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
}
if (databaseSchemaVersion < 10 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)');
}
if (databaseSchemaVersion < 9 && isBitcoin === true) {
this.uniqueLog(logger.notice, this.hashratesTruncatedMessage);
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)');
await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)');
}
if (databaseSchemaVersion < 11 && isBitcoin === true) {
logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.`);
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
await this.$executeQuery(`ALTER TABLE blocks
ADD avg_fee INT UNSIGNED NULL,
ADD avg_fee_rate INT UNSIGNED NULL
`);
await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"');
}
if (databaseSchemaVersion < 10 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)');
}
if (databaseSchemaVersion < 12 && isBitcoin === true) {
// No need to re-index because the new data type can contain larger values
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
}
if (databaseSchemaVersion < 11 && isBitcoin === true) {
this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
await this.$executeQuery(`ALTER TABLE blocks
ADD avg_fee INT UNSIGNED NULL,
ADD avg_fee_rate INT UNSIGNED NULL
`);
await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"');
}
if (databaseSchemaVersion < 13 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` DOUBLE UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
}
if (databaseSchemaVersion < 12 && isBitcoin === true) {
// No need to re-index because the new data type can contain larger values
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
}
if (databaseSchemaVersion < 14 && isBitcoin === true) {
logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.`);
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`');
await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"');
}
if (databaseSchemaVersion < 13 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` DOUBLE UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
}
if (databaseSchemaVersion < 16 && isBitcoin === true) {
logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.`);
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index because we changed timestamps
}
if (databaseSchemaVersion < 14 && isBitcoin === true) {
this.uniqueLog(logger.notice, this.hashratesTruncatedMessage);
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`');
await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"');
}
if (databaseSchemaVersion < 17 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL');
}
if (databaseSchemaVersion < 16 && isBitcoin === true) {
this.uniqueLog(logger.notice, this.hashratesTruncatedMessage);
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index because we changed timestamps
}
if (databaseSchemaVersion < 18 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);');
}
if (databaseSchemaVersion < 17 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL');
}
if (databaseSchemaVersion < 19) {
await this.$executeQuery(this.getCreateRatesTableQuery(), await this.$checkIfTableExists('rates'));
if (databaseSchemaVersion < 18 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);');
}
if (databaseSchemaVersion < 19) {
await this.$executeQuery(this.getCreateRatesTableQuery(), await this.$checkIfTableExists('rates'));
}
if (databaseSchemaVersion < 20 && isBitcoin === true) {
await this.$executeQuery(this.getCreateBlocksSummariesTableQuery(), await this.$checkIfTableExists('blocks_summaries'));
}
if (databaseSchemaVersion < 21) {
await this.$executeQuery('DROP TABLE IF EXISTS `rates`');
await this.$executeQuery(this.getCreatePricesTableQuery(), await this.$checkIfTableExists('prices'));
}
if (databaseSchemaVersion < 22 && isBitcoin === true) {
await this.$executeQuery('DROP TABLE IF EXISTS `difficulty_adjustments`');
await this.$executeQuery(this.getCreateDifficultyAdjustmentsTableQuery(), await this.$checkIfTableExists('difficulty_adjustments'));
}
if (databaseSchemaVersion < 23) {
await this.$executeQuery('TRUNCATE `prices`');
await this.$executeQuery('ALTER TABLE `prices` DROP `avg_prices`');
await this.$executeQuery('ALTER TABLE `prices` ADD `USD` float DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `prices` ADD `EUR` float DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `prices` ADD `GBP` float DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `prices` ADD `CAD` float DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `prices` ADD `CHF` float DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `prices` ADD `AUD` float DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `prices` ADD `JPY` float DEFAULT "0"');
}
if (databaseSchemaVersion < 24 && isBitcoin == true) {
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`');
await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits'));
}
if (databaseSchemaVersion < 25 && isBitcoin === true) {
await this.$executeQuery(this.getCreateLightningStatisticsQuery(), await this.$checkIfTableExists('lightning_stats'));
await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats'));
}
if (databaseSchemaVersion < 26 && isBitcoin === true) {
if (config.LIGHTNING.ENABLED) {
this.uniqueLog(logger.notice, `'lightning_stats' table has been truncated.`);
}
} catch (e) {
throw e;
await this.$executeQuery(`TRUNCATE lightning_stats`);
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD unannounced_nodes int(11) NOT NULL DEFAULT "0"');
}
if (databaseSchemaVersion < 27 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
}
if (databaseSchemaVersion < 28 && isBitcoin === true) {
if (config.LIGHTNING.ENABLED) {
this.uniqueLog(logger.notice, `'lightning_stats' and 'node_stats' tables have been truncated.`);
}
await this.$executeQuery(`TRUNCATE lightning_stats`);
await this.$executeQuery(`TRUNCATE node_stats`);
await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`);
}
if (databaseSchemaVersion < 29 && isBitcoin === true) {
await this.$executeQuery(this.getCreateGeoNamesTableQuery(), await this.$checkIfTableExists('geo_names'));
await this.$executeQuery('ALTER TABLE `nodes` ADD as_number int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD city_id int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD country_id int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD accuracy_radius int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD subdivision_id int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD longitude double NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD latitude double NULL DEFAULT NULL');
}
if (databaseSchemaVersion < 30 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization") NOT NULL');
}
if (databaseSchemaVersion < 31 && isBitcoin == true) { // Link blocks to prices
await this.$executeQuery('ALTER TABLE `prices` ADD `id` int NULL AUTO_INCREMENT UNIQUE');
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_prices`');
await this.$executeQuery(this.getCreateBlocksPricesTableQuery(), await this.$checkIfTableExists('blocks_prices'));
}
if (databaseSchemaVersion < 32 && isBitcoin == true) {
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD `template` JSON DEFAULT "[]"');
}
if (databaseSchemaVersion < 33 && isBitcoin == true) {
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization", "country_iso_code") NOT NULL');
}
if (databaseSchemaVersion < 34 && isBitcoin == true) {
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_tor_nodes int(11) NOT NULL DEFAULT "0"');
}
if (databaseSchemaVersion < 35 && isBitcoin == true) {
await this.$executeQuery('DELETE from `lightning_stats` WHERE added > "2021-09-19"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD CONSTRAINT added_unique UNIQUE (added);');
}
if (databaseSchemaVersion < 36 && isBitcoin == true) {
await this.$executeQuery('ALTER TABLE `nodes` ADD status TINYINT NOT NULL DEFAULT "1"');
}
if (databaseSchemaVersion < 37 && isBitcoin == true) {
await this.$executeQuery(this.getCreateLNNodesSocketsTableQuery(), await this.$checkIfTableExists('nodes_sockets'));
}
if (databaseSchemaVersion < 38 && isBitcoin == true) {
if (config.LIGHTNING.ENABLED) {
this.uniqueLog(logger.notice, `'lightning_stats' and 'node_stats' tables have been truncated.`);
}
await this.$executeQuery(`TRUNCATE lightning_stats`);
await this.$executeQuery(`TRUNCATE node_stats`);
await this.$executeQuery('ALTER TABLE `lightning_stats` CHANGE `added` `added` timestamp NULL');
await this.$executeQuery('ALTER TABLE `node_stats` CHANGE `added` `added` timestamp NULL');
}
if (databaseSchemaVersion < 39 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `nodes` ADD alias_search TEXT NULL DEFAULT NULL AFTER `alias`');
await this.$executeQuery('ALTER TABLE nodes ADD FULLTEXT(alias_search)');
}
if (databaseSchemaVersion < 40 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `nodes` ADD capacity bigint(20) unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD channels int(11) unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD INDEX `capacity` (`capacity`);');
}
}
@@ -228,7 +385,7 @@ class DatabaseMigration {
/**
* Small query execution wrapper to log all executed queries
*/
private async $executeQuery(query: string, silent: boolean = false): Promise<any> {
private async $executeQuery(query: string, silent = false): Promise<any> {
if (!silent) {
logger.debug('MIGRATIONS: Execute query:\n' + query);
}
@@ -257,21 +414,17 @@ class DatabaseMigration {
* Create the `state` table
*/
private async $createMigrationStateTable(): Promise<void> {
try {
const query = `CREATE TABLE IF NOT EXISTS state (
name varchar(25) NOT NULL,
number int(11) NULL,
string varchar(100) NULL,
CONSTRAINT name_unique UNIQUE (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
await this.$executeQuery(query);
const query = `CREATE TABLE IF NOT EXISTS state (
name varchar(25) NOT NULL,
number int(11) NULL,
string varchar(100) NULL,
CONSTRAINT name_unique UNIQUE (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
await this.$executeQuery(query);
// Set initial values
await this.$executeQuery(`INSERT INTO state VALUES('schema_version', 0, NULL);`);
await this.$executeQuery(`INSERT INTO state VALUES('last_elements_block', 0, NULL);`);
} catch (e) {
throw e;
}
// Set initial values
await this.$executeQuery(`INSERT INTO state VALUES('schema_version', 0, NULL);`);
await this.$executeQuery(`INSERT INTO state VALUES('last_elements_block', 0, NULL);`);
}
/**
@@ -282,6 +435,8 @@ class DatabaseMigration {
for (const query of this.getMigrationQueriesFromVersion(version)) {
transactionQueries.push(query);
}
logger.notice(`MIGRATIONS: ${version > 0 ? 'Upgrading' : 'Initializing'} database schema version number to ${DatabaseMigration.currentVersion}`);
transactionQueries.push(this.getUpdateToLatestSchemaVersionQuery());
try {
@@ -305,6 +460,9 @@ class DatabaseMigration {
if (version < 1) {
if (config.MEMPOOL.NETWORK !== 'liquid' && config.MEMPOOL.NETWORK !== 'liquidtestnet') {
if (version > 0) {
logger.notice(`MIGRATIONS: Migrating (shifting) statistics table data`);
}
queries.push(this.getShiftStatisticsQuery());
}
}
@@ -470,7 +628,7 @@ class DatabaseMigration {
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreateRatesTableQuery(): string {
private getCreateRatesTableQuery(): string { // This table has been replaced by the prices table
return `CREATE TABLE IF NOT EXISTS rates (
height int(10) unsigned NOT NULL,
bisq_rates JSON NOT NULL,
@@ -478,8 +636,155 @@ class DatabaseMigration {
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreateBlocksSummariesTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS blocks_summaries (
height int(10) unsigned NOT NULL,
id varchar(65) NOT NULL,
transactions JSON NOT NULL,
PRIMARY KEY (id),
INDEX (height)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreatePricesTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS prices (
time timestamp NOT NULL,
avg_prices JSON NOT NULL,
PRIMARY KEY (time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreateDifficultyAdjustmentsTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS difficulty_adjustments (
time timestamp NOT NULL,
height int(10) unsigned NOT NULL,
difficulty double unsigned NOT NULL,
adjustment float NOT NULL,
PRIMARY KEY (height),
INDEX (time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreateLightningStatisticsQuery(): string {
return `CREATE TABLE IF NOT EXISTS lightning_stats (
id int(11) NOT NULL AUTO_INCREMENT,
added datetime NOT NULL,
channel_count int(11) NOT NULL,
node_count int(11) NOT NULL,
total_capacity double unsigned NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreateNodesQuery(): string {
return `CREATE TABLE IF NOT EXISTS nodes (
public_key varchar(66) NOT NULL,
first_seen datetime NOT NULL,
updated_at datetime NOT NULL,
alias varchar(200) CHARACTER SET utf8mb4 NOT NULL,
color varchar(200) NOT NULL,
sockets text DEFAULT NULL,
PRIMARY KEY (public_key),
KEY alias (alias(10))
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreateChannelsQuery(): string {
return `CREATE TABLE IF NOT EXISTS channels (
id bigint(11) unsigned NOT NULL,
short_id varchar(15) NOT NULL DEFAULT '',
capacity bigint(20) unsigned NOT NULL,
transaction_id varchar(64) NOT NULL,
transaction_vout int(11) NOT NULL,
updated_at datetime DEFAULT NULL,
created datetime DEFAULT NULL,
status int(11) NOT NULL DEFAULT 0,
closing_transaction_id varchar(64) DEFAULT NULL,
closing_date datetime DEFAULT NULL,
closing_reason int(11) DEFAULT NULL,
node1_public_key varchar(66) NOT NULL,
node1_base_fee_mtokens bigint(20) unsigned DEFAULT NULL,
node1_cltv_delta int(11) DEFAULT NULL,
node1_fee_rate bigint(11) DEFAULT NULL,
node1_is_disabled tinyint(1) DEFAULT NULL,
node1_max_htlc_mtokens bigint(20) unsigned DEFAULT NULL,
node1_min_htlc_mtokens bigint(20) DEFAULT NULL,
node1_updated_at datetime DEFAULT NULL,
node2_public_key varchar(66) NOT NULL,
node2_base_fee_mtokens bigint(20) unsigned DEFAULT NULL,
node2_cltv_delta int(11) DEFAULT NULL,
node2_fee_rate bigint(11) DEFAULT NULL,
node2_is_disabled tinyint(1) DEFAULT NULL,
node2_max_htlc_mtokens bigint(20) unsigned DEFAULT NULL,
node2_min_htlc_mtokens bigint(20) unsigned DEFAULT NULL,
node2_updated_at datetime DEFAULT NULL,
PRIMARY KEY (id),
KEY node1_public_key (node1_public_key),
KEY node2_public_key (node2_public_key),
KEY status (status),
KEY short_id (short_id),
KEY transaction_id (transaction_id),
KEY closing_transaction_id (closing_transaction_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreateNodesStatsQuery(): string {
return `CREATE TABLE IF NOT EXISTS node_stats (
id int(11) unsigned NOT NULL AUTO_INCREMENT,
public_key varchar(66) NOT NULL DEFAULT '',
added date NOT NULL,
capacity bigint(20) unsigned NOT NULL DEFAULT 0,
channels int(11) unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (id),
UNIQUE KEY added (added,public_key),
KEY public_key (public_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreateBlocksAuditsTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS blocks_audits (
time timestamp NOT NULL,
hash varchar(65) NOT NULL,
height int(10) unsigned NOT NULL,
missing_txs JSON NOT NULL,
added_txs JSON NOT NULL,
match_rate float unsigned NOT NULL,
PRIMARY KEY (hash),
INDEX (height)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreateGeoNamesTableQuery(): string {
return `CREATE TABLE geo_names (
id int(11) unsigned NOT NULL,
type enum('city','country','division','continent') NOT NULL,
names text DEFAULT NULL,
UNIQUE KEY id (id,type),
KEY id_2 (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreateBlocksPricesTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS blocks_prices (
height int(10) unsigned NOT NULL,
price_id int(10) unsigned NOT NULL,
PRIMARY KEY (height),
INDEX (price_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreateLNNodesSocketsTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS nodes_sockets (
public_key varchar(66) NOT NULL,
socket varchar(100) NOT NULL,
type enum('ipv4', 'ipv6', 'torv2', 'torv3', 'i2p', 'dns', 'websocket') NULL,
UNIQUE KEY public_key_socket (public_key, socket),
INDEX (public_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
public async $truncateIndexedData(tables: string[]) {
const allowedTables = ['blocks', 'hashrates'];
const allowedTables = ['blocks', 'hashrates', 'prices'];
try {
for (const table of tables) {

View File

@@ -2,65 +2,100 @@ import config from '../config';
import { IDifficultyAdjustment } from '../mempool.interfaces';
import blocks from './blocks';
class DifficultyAdjustmentApi {
constructor() { }
export interface DifficultyAdjustment {
progressPercent: number; // Percent: 0 to 100
difficultyChange: number; // Percent: -75 to 300
estimatedRetargetDate: number; // Unix time in ms
remainingBlocks: number; // Block count
remainingTime: number; // Duration of time in ms
previousRetarget: number; // Percent: -75 to 300
nextRetargetHeight: number; // Block Height
timeAvg: number; // Duration of time in ms
timeOffset: number; // (Testnet) Time since last block (cap @ 20min) in ms
}
public getDifficultyAdjustment(): IDifficultyAdjustment {
export function calcDifficultyAdjustment(
DATime: number,
nowSeconds: number,
blockHeight: number,
previousRetarget: number,
network: string,
latestBlockTimestamp: number,
): DifficultyAdjustment {
const ESTIMATE_LAG_BLOCKS = 146; // For first 7.2% of epoch, don't estimate.
const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
const BLOCK_SECONDS_TARGET = 600; // Bitcoin mainnet
const TESTNET_MAX_BLOCK_SECONDS = 1200; // Bitcoin testnet
const diffSeconds = nowSeconds - DATime;
const blocksInEpoch = (blockHeight >= 0) ? blockHeight % EPOCH_BLOCK_LENGTH : 0;
const progressPercent = (blockHeight >= 0) ? blocksInEpoch / EPOCH_BLOCK_LENGTH * 100 : 100;
const remainingBlocks = EPOCH_BLOCK_LENGTH - blocksInEpoch;
const nextRetargetHeight = (blockHeight >= 0) ? blockHeight + remainingBlocks : 0;
let difficultyChange = 0;
let timeAvgSecs = BLOCK_SECONDS_TARGET;
// Only calculate the estimate once we have 7.2% of blocks in current epoch
if (blocksInEpoch >= ESTIMATE_LAG_BLOCKS) {
timeAvgSecs = diffSeconds / blocksInEpoch;
difficultyChange = (BLOCK_SECONDS_TARGET / timeAvgSecs - 1) * 100;
// Max increase is x4 (+300%)
if (difficultyChange > 300) {
difficultyChange = 300;
}
// Max decrease is /4 (-75%)
if (difficultyChange < -75) {
difficultyChange = -75;
}
}
// Testnet difficulty is set to 1 after 20 minutes of no blocks,
// therefore the time between blocks will always be below 20 minutes (1200s).
let timeOffset = 0;
if (network === 'testnet') {
if (timeAvgSecs > TESTNET_MAX_BLOCK_SECONDS) {
timeAvgSecs = TESTNET_MAX_BLOCK_SECONDS;
}
const secondsSinceLastBlock = nowSeconds - latestBlockTimestamp;
if (secondsSinceLastBlock + timeAvgSecs > TESTNET_MAX_BLOCK_SECONDS) {
timeOffset = -Math.min(secondsSinceLastBlock, TESTNET_MAX_BLOCK_SECONDS) * 1000;
}
}
const timeAvg = Math.floor(timeAvgSecs * 1000);
const remainingTime = remainingBlocks * timeAvg;
const estimatedRetargetDate = remainingTime + nowSeconds * 1000;
return {
progressPercent,
difficultyChange,
estimatedRetargetDate,
remainingBlocks,
remainingTime,
previousRetarget,
nextRetargetHeight,
timeAvg,
timeOffset,
};
}
class DifficultyAdjustmentApi {
public getDifficultyAdjustment(): IDifficultyAdjustment | null {
const DATime = blocks.getLastDifficultyAdjustmentTime();
const previousRetarget = blocks.getPreviousDifficultyRetarget();
const blockHeight = blocks.getCurrentBlockHeight();
const blocksCache = blocks.getBlocks();
const latestBlock = blocksCache[blocksCache.length - 1];
const now = new Date().getTime() / 1000;
const diff = now - DATime;
const blocksInEpoch = blockHeight % 2016;
const progressPercent = (blocksInEpoch >= 0) ? blocksInEpoch / 2016 * 100 : 100;
const remainingBlocks = 2016 - blocksInEpoch;
const nextRetargetHeight = blockHeight + remainingBlocks;
let difficultyChange = 0;
if (remainingBlocks < 1870) {
if (blocksInEpoch > 0) {
difficultyChange = (600 / (diff / blocksInEpoch) - 1) * 100;
}
if (difficultyChange > 300) {
difficultyChange = 300;
}
if (difficultyChange < -75) {
difficultyChange = -75;
}
if (!latestBlock) {
return null;
}
const nowSeconds = Math.floor(new Date().getTime() / 1000);
let timeAvgMins = blocksInEpoch && blocksInEpoch > 146 ? diff / blocksInEpoch / 60 : 10;
// Testnet difficulty is set to 1 after 20 minutes of no blocks,
// therefore the time between blocks will always be below 20 minutes (1200s).
let timeOffset = 0;
if (config.MEMPOOL.NETWORK === 'testnet') {
if (timeAvgMins > 20) {
timeAvgMins = 20;
}
if (now - latestBlock.timestamp + timeAvgMins * 60 > 1200) {
timeOffset = -Math.min(now - latestBlock.timestamp, 1200) * 1000;
}
}
const timeAvg = timeAvgMins * 60 * 1000 ;
const remainingTime = (remainingBlocks * timeAvg) + (now * 1000);
const estimatedRetargetDate = remainingTime + now;
return {
progressPercent,
difficultyChange,
estimatedRetargetDate,
remainingBlocks,
remainingTime,
previousRetarget,
nextRetargetHeight,
timeAvg,
timeOffset,
};
return calcDifficultyAdjustment(
DATime, nowSeconds, blockHeight, previousRetarget,
config.MEMPOOL.NETWORK, latestBlock.timestamp
);
}
}

View File

@@ -1,6 +1,6 @@
import * as fs from 'fs';
const fsPromises = fs.promises;
import * as cluster from 'cluster';
import cluster from 'cluster';
import memPool from './mempool';
import blocks from './blocks';
import logger from '../logger';
@@ -19,7 +19,7 @@ class DiskCache {
constructor() { }
async $saveCacheToDisk(): Promise<void> {
if (!cluster.isMaster) {
if (!cluster.isPrimary) {
return;
}
if (this.isWritingCache) {
@@ -43,14 +43,15 @@ class DiskCache {
await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({
cacheSchemaVersion: this.cacheSchemaVersion,
blocks: blocks.getBlocks(),
blockSummaries: blocks.getBlockSummaries(),
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), {flag: 'w'});
}), { 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'});
}), { flag: 'w' });
}
logger.debug('Mempool and blocks data saved to disk cache');
this.isWritingCache = false;
@@ -66,7 +67,7 @@ class DiskCache {
fs.unlinkSync(DiskCache.FILE_NAMES.replace('{number}', i.toString()));
}
}
loadMempoolCache() {
if (!fs.existsSync(DiskCache.FILE_NAME)) {
return;
@@ -109,6 +110,7 @@ class DiskCache {
memPool.setMempool(data.mempool);
blocks.setBlocks(data.blocks);
blocks.setBlockSummaries(data.blockSummaries || []);
} catch (e) {
logger.warn('Failed to parse mempoool and blocks cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
}

View File

@@ -0,0 +1,562 @@
import logger from '../../logger';
import DB from '../../database';
import nodesApi from './nodes.api';
import { ResultSetHeader } from 'mysql2';
import { ILightningApi } from '../lightning/lightning-api.interface';
import { Common } from '../common';
class ChannelsApi {
public async $getAllChannels(): Promise<any[]> {
try {
const query = `SELECT * FROM channels`;
const [rows]: any = await DB.query(query);
return rows;
} catch (e) {
logger.err('$getAllChannels error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getAllChannelsGeo(publicKey?: string, style?: string): Promise<any[]> {
try {
let select: string;
if (style === 'widget') {
select = `
nodes_1.latitude AS node1_latitude, nodes_1.longitude AS node1_longitude,
nodes_2.latitude AS node2_latitude, nodes_2.longitude AS node2_longitude
`;
} else {
select = `
nodes_1.public_key as node1_public_key, nodes_1.alias AS node1_alias,
nodes_1.latitude AS node1_latitude, nodes_1.longitude AS node1_longitude,
nodes_2.public_key as node2_public_key, nodes_2.alias AS node2_alias,
nodes_2.latitude AS node2_latitude, nodes_2.longitude AS node2_longitude
`;
}
const params: string[] = [];
let query = `SELECT ${select}
FROM channels
JOIN nodes AS nodes_1 on nodes_1.public_key = channels.node1_public_key
JOIN nodes AS nodes_2 on nodes_2.public_key = channels.node2_public_key
WHERE channels.status = 1
AND nodes_1.latitude IS NOT NULL AND nodes_1.longitude IS NOT NULL
AND nodes_2.latitude IS NOT NULL AND nodes_2.longitude IS NOT NULL
`;
if (publicKey !== undefined) {
query += ' AND (nodes_1.public_key = ? OR nodes_2.public_key = ?)';
params.push(publicKey);
params.push(publicKey);
} else {
query += ` AND channels.capacity > 1000000
GROUP BY nodes_1.public_key, nodes_2.public_key
ORDER BY channels.capacity DESC
LIMIT 10000
`;
}
const [rows]: any = await DB.query(query, params);
return rows.map((row) => {
if (style === 'widget') {
return [
row.node1_longitude, row.node1_latitude,
row.node2_longitude, row.node2_latitude,
];
} else {
return [
row.node1_public_key, row.node1_alias,
row.node1_longitude, row.node1_latitude,
row.node2_public_key, row.node2_alias,
row.node2_longitude, row.node2_latitude,
];
}
});
} catch (e) {
logger.err('$getAllChannelsGeo error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $searchChannelsById(search: string): Promise<any[]> {
try {
const searchStripped = search.replace('%', '') + '%';
const query = `SELECT id, short_id, capacity, status FROM channels WHERE id LIKE ? OR short_id LIKE ? LIMIT 10`;
const [rows]: any = await DB.query(query, [searchStripped, searchStripped]);
return rows;
} catch (e) {
logger.err('$searchChannelsById error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getChannelsByStatus(status: number | number[]): Promise<any[]> {
try {
let query: string;
if (Array.isArray(status)) {
query = `SELECT * FROM channels WHERE status IN (${status.join(',')})`;
} else {
query = `SELECT * FROM channels WHERE status = ?`;
}
const [rows]: any = await DB.query(query, [status]);
return rows;
} catch (e) {
logger.err('$getChannelsByStatus error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getClosedChannelsWithoutReason(): Promise<any[]> {
try {
const query = `SELECT * FROM channels WHERE status = 2 AND closing_reason IS NULL AND closing_transaction_id != ''`;
const [rows]: any = await DB.query(query);
return rows;
} catch (e) {
logger.err('$getClosedChannelsWithoutReason error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getChannelsWithoutCreatedDate(): Promise<any[]> {
try {
const query = `SELECT * FROM channels WHERE created IS NULL`;
const [rows]: any = await DB.query(query);
return rows;
} catch (e) {
logger.err('$getChannelsWithoutCreatedDate error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getChannel(id: string): Promise<any> {
try {
const query = `
SELECT n1.alias AS alias_left, n1.longitude as node1_longitude, n1.latitude as node1_latitude,
n2.alias AS alias_right, n2.longitude as node2_longitude, n2.latitude as node2_latitude,
channels.*,
ns1.channels AS channels_left, ns1.capacity AS capacity_left, ns2.channels AS channels_right, ns2.capacity AS capacity_right
FROM channels
LEFT JOIN nodes AS n1 ON n1.public_key = channels.node1_public_key
LEFT JOIN nodes AS n2 ON n2.public_key = channels.node2_public_key
LEFT JOIN node_stats AS ns1 ON ns1.public_key = channels.node1_public_key
LEFT JOIN node_stats AS ns2 ON ns2.public_key = channels.node2_public_key
WHERE (
ns1.id = (
SELECT MAX(id)
FROM node_stats
WHERE public_key = channels.node1_public_key
)
AND ns2.id = (
SELECT MAX(id)
FROM node_stats
WHERE public_key = channels.node2_public_key
)
)
AND channels.id = ?
`;
const [rows]: any = await DB.query(query, [id]);
if (rows[0]) {
return this.convertChannel(rows[0]);
}
} catch (e) {
logger.err('$getChannel error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getChannelsStats(): Promise<any> {
try {
// Feedback from zerofeerouting:
// "I would argue > 5000ppm can be ignored. Channels charging more than .5% fee are ignored by CLN for example."
const ignoredFeeRateThreshold = 5000;
const ignoredBaseFeeThreshold = 5000;
// Capacity
let query = `SELECT AVG(capacity) AS avgCapacity FROM channels WHERE status = 1 ORDER BY capacity`;
const [avgCapacity]: any = await DB.query(query);
query = `SELECT capacity FROM channels WHERE status = 1 ORDER BY capacity`;
let [capacity]: any = await DB.query(query);
capacity = capacity.map(capacity => capacity.capacity);
const medianCapacity = capacity[Math.floor(capacity.length / 2)];
// Fee rates
query = `SELECT node1_fee_rate FROM channels WHERE node1_fee_rate < ${ignoredFeeRateThreshold} AND status = 1`;
let [feeRates1]: any = await DB.query(query);
feeRates1 = feeRates1.map(rate => rate.node1_fee_rate);
query = `SELECT node2_fee_rate FROM channels WHERE node2_fee_rate < ${ignoredFeeRateThreshold} AND status = 1`;
let [feeRates2]: any = await DB.query(query);
feeRates2 = feeRates2.map(rate => rate.node2_fee_rate);
let feeRates = (feeRates1.concat(feeRates2)).sort((a, b) => a - b);
let avgFeeRate = 0;
for (const rate of feeRates) {
avgFeeRate += rate;
}
avgFeeRate /= feeRates.length;
const medianFeeRate = feeRates[Math.floor(feeRates.length / 2)];
// Base fees
query = `SELECT node1_base_fee_mtokens FROM channels WHERE node1_base_fee_mtokens < ${ignoredBaseFeeThreshold} AND status = 1`;
let [baseFees1]: any = await DB.query(query);
baseFees1 = baseFees1.map(rate => rate.node1_base_fee_mtokens);
query = `SELECT node2_base_fee_mtokens FROM channels WHERE node2_base_fee_mtokens < ${ignoredBaseFeeThreshold} AND status = 1`;
let [baseFees2]: any = await DB.query(query);
baseFees2 = baseFees2.map(rate => rate.node2_base_fee_mtokens);
let baseFees = (baseFees1.concat(baseFees2)).sort((a, b) => a - b);
let avgBaseFee = 0;
for (const fee of baseFees) {
avgBaseFee += fee;
}
avgBaseFee /= baseFees.length;
const medianBaseFee = feeRates[Math.floor(baseFees.length / 2)];
return {
avgCapacity: parseInt(avgCapacity[0].avgCapacity, 10),
avgFeeRate: avgFeeRate,
avgBaseFee: avgBaseFee,
medianCapacity: medianCapacity,
medianFeeRate: medianFeeRate,
medianBaseFee: medianBaseFee,
}
} catch (e) {
logger.err(`Cannot calculate channels statistics. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
}
}
public async $getChannelsByTransactionId(transactionIds: string[]): Promise<any[]> {
try {
const query = `
SELECT n1.alias AS alias_left, n2.alias AS alias_right, channels.*
FROM channels
LEFT JOIN nodes AS n1 ON n1.public_key = channels.node1_public_key
LEFT JOIN nodes AS n2 ON n2.public_key = channels.node2_public_key
WHERE channels.transaction_id IN ? OR channels.closing_transaction_id IN ?
`;
const [rows]: any = await DB.query(query, [[transactionIds], [transactionIds]]);
const channels = rows.map((row) => this.convertChannel(row));
return channels;
} catch (e) {
logger.err('$getChannelByTransactionId error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getChannelsForNode(public_key: string, index: number, length: number, status: string): Promise<any[]> {
try {
let channelStatusFilter;
if (status === 'open') {
channelStatusFilter = '< 2';
} else if (status === 'active') {
channelStatusFilter = '= 1';
} else if (status === 'closed') {
channelStatusFilter = '= 2';
} else {
throw new Error('getChannelsForNode: Invalid status requested');
}
// Channels originating from node
let query = `
SELECT COALESCE(node2.alias, SUBSTRING(node2_public_key, 0, 20)) AS alias, COALESCE(node2.public_key, node2_public_key) AS public_key,
channels.status, channels.node1_fee_rate,
channels.capacity, channels.short_id, channels.id, channels.closing_reason,
UNIX_TIMESTAMP(closing_date) as closing_date, UNIX_TIMESTAMP(channels.updated_at) as updated_at
FROM channels
LEFT JOIN nodes AS node2 ON node2.public_key = channels.node2_public_key
WHERE node1_public_key = ? AND channels.status ${channelStatusFilter}
`;
const [channelsFromNode]: any = await DB.query(query, [public_key]);
// Channels incoming to node
query = `
SELECT COALESCE(node1.alias, SUBSTRING(node1_public_key, 0, 20)) AS alias, COALESCE(node1.public_key, node1_public_key) AS public_key,
channels.status, channels.node2_fee_rate,
channels.capacity, channels.short_id, channels.id, channels.closing_reason,
UNIX_TIMESTAMP(closing_date) as closing_date, UNIX_TIMESTAMP(channels.updated_at) as updated_at
FROM channels
LEFT JOIN nodes AS node1 ON node1.public_key = channels.node1_public_key
WHERE node2_public_key = ? AND channels.status ${channelStatusFilter}
`;
const [channelsToNode]: any = await DB.query(query, [public_key]);
let allChannels = channelsFromNode.concat(channelsToNode);
allChannels.sort((a, b) => {
if (status === 'closed') {
if (!b.closing_date && !a.closing_date) {
return (b.updated_at ?? 0) - (a.updated_at ?? 0);
} else {
return (b.closing_date ?? 0) - (a.closing_date ?? 0);
}
} else {
return b.capacity - a.capacity;
}
});
if (index >= 0) {
allChannels = allChannels.slice(index, index + length);
} else if (index === -1) { // Node channels tree chart
allChannels = allChannels.slice(0, 1000);
}
const channels: any[] = []
for (const row of allChannels) {
let channel;
if (index >= 0) {
const activeChannelsStats: any = await nodesApi.$getActiveChannelsStats(row.public_key);
channel = {
status: row.status,
closing_reason: row.closing_reason,
closing_date: row.closing_date,
capacity: row.capacity ?? 0,
short_id: row.short_id,
id: row.id,
fee_rate: row.node1_fee_rate ?? row.node2_fee_rate ?? 0,
node: {
alias: row.alias.length > 0 ? row.alias : row.public_key.slice(0, 20),
public_key: row.public_key,
channels: activeChannelsStats.active_channel_count ?? 0,
capacity: activeChannelsStats.capacity ?? 0,
}
};
} else if (index === -1) {
channel = {
capacity: row.capacity ?? 0,
short_id: row.short_id,
id: row.id,
node: {
alias: row.alias.length > 0 ? row.alias : row.public_key.slice(0, 20),
public_key: row.public_key,
}
};
}
channels.push(channel);
}
return channels;
} catch (e) {
logger.err('$getChannelsForNode error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getChannelsCountForNode(public_key: string, status: string): Promise<any> {
try {
// Default active and inactive channels
let statusQuery = '< 2';
// Closed channels only
if (status === 'closed') {
statusQuery = '= 2';
}
const query = `
SELECT COUNT(*) AS count
FROM channels
WHERE (node1_public_key = ? OR node2_public_key = ?)
AND status ${statusQuery}
`;
const [rows]: any = await DB.query(query, [public_key, public_key]);
return rows[0]['count'];
} catch (e) {
logger.err('$getChannelsForNode error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
private convertChannel(channel: any): any {
return {
'id': channel.id,
'short_id': channel.short_id,
'capacity': channel.capacity,
'transaction_id': channel.transaction_id,
'transaction_vout': channel.transaction_vout,
'closing_transaction_id': channel.closing_transaction_id,
'closing_reason': channel.closing_reason,
'closing_date': channel.closing_date,
'updated_at': channel.updated_at,
'created': channel.created,
'status': channel.status,
'node_left': {
'alias': channel.alias_left,
'public_key': channel.node1_public_key,
'channels': channel.channels_left,
'capacity': channel.capacity_left,
'base_fee_mtokens': channel.node1_base_fee_mtokens,
'cltv_delta': channel.node1_cltv_delta,
'fee_rate': channel.node1_fee_rate,
'is_disabled': channel.node1_is_disabled,
'max_htlc_mtokens': channel.node1_max_htlc_mtokens,
'min_htlc_mtokens': channel.node1_min_htlc_mtokens,
'updated_at': channel.node1_updated_at,
'longitude': channel.node1_longitude,
'latitude': channel.node1_latitude,
},
'node_right': {
'alias': channel.alias_right,
'public_key': channel.node2_public_key,
'channels': channel.channels_right,
'capacity': channel.capacity_right,
'base_fee_mtokens': channel.node2_base_fee_mtokens,
'cltv_delta': channel.node2_cltv_delta,
'fee_rate': channel.node2_fee_rate,
'is_disabled': channel.node2_is_disabled,
'max_htlc_mtokens': channel.node2_max_htlc_mtokens,
'min_htlc_mtokens': channel.node2_min_htlc_mtokens,
'updated_at': channel.node2_updated_at,
'longitude': channel.node2_longitude,
'latitude': channel.node2_latitude,
},
};
}
/**
* Save or update a channel present in the graph
*/
public async $saveChannel(channel: ILightningApi.Channel, status = 1): Promise<void> {
const [ txid, vout ] = channel.chan_point.split(':');
const policy1: Partial<ILightningApi.RoutingPolicy> = channel.node1_policy || {};
const policy2: Partial<ILightningApi.RoutingPolicy> = channel.node2_policy || {};
const query = `INSERT INTO channels
(
id,
short_id,
capacity,
transaction_id,
transaction_vout,
updated_at,
status,
node1_public_key,
node1_base_fee_mtokens,
node1_cltv_delta,
node1_fee_rate,
node1_is_disabled,
node1_max_htlc_mtokens,
node1_min_htlc_mtokens,
node1_updated_at,
node2_public_key,
node2_base_fee_mtokens,
node2_cltv_delta,
node2_fee_rate,
node2_is_disabled,
node2_max_htlc_mtokens,
node2_min_htlc_mtokens,
node2_updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ${status}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
capacity = ?,
updated_at = ?,
status = ${status},
node1_public_key = ?,
node1_base_fee_mtokens = ?,
node1_cltv_delta = ?,
node1_fee_rate = ?,
node1_is_disabled = ?,
node1_max_htlc_mtokens = ?,
node1_min_htlc_mtokens = ?,
node1_updated_at = ?,
node2_public_key = ?,
node2_base_fee_mtokens = ?,
node2_cltv_delta = ?,
node2_fee_rate = ?,
node2_is_disabled = ?,
node2_max_htlc_mtokens = ?,
node2_min_htlc_mtokens = ?,
node2_updated_at = ?
;`;
await DB.query(query, [
Common.channelShortIdToIntegerId(channel.channel_id),
Common.channelIntegerIdToShortId(channel.channel_id),
channel.capacity,
txid,
vout,
Common.utcDateToMysql(channel.last_update),
channel.node1_pub,
policy1.fee_base_msat,
policy1.time_lock_delta,
policy1.fee_rate_milli_msat,
policy1.disabled,
policy1.max_htlc_msat,
policy1.min_htlc,
Common.utcDateToMysql(policy1.last_update),
channel.node2_pub,
policy2.fee_base_msat,
policy2.time_lock_delta,
policy2.fee_rate_milli_msat,
policy2.disabled,
policy2.max_htlc_msat,
policy2.min_htlc,
Common.utcDateToMysql(policy2.last_update),
channel.capacity,
Common.utcDateToMysql(channel.last_update),
channel.node1_pub,
policy1.fee_base_msat,
policy1.time_lock_delta,
policy1.fee_rate_milli_msat,
policy1.disabled,
policy1.max_htlc_msat,
policy1.min_htlc,
Common.utcDateToMysql(policy1.last_update),
channel.node2_pub,
policy2.fee_base_msat,
policy2.time_lock_delta,
policy2.fee_rate_milli_msat,
policy2.disabled,
policy2.max_htlc_msat,
policy2.min_htlc,
Common.utcDateToMysql(policy2.last_update)
]);
}
/**
* Set all channels not in `graphChannelsIds` as inactive (status = 0)
*/
public async $setChannelsInactive(graphChannelsIds: string[]): Promise<void> {
if (graphChannelsIds.length === 0) {
return;
}
try {
const result = await DB.query<ResultSetHeader>(`
UPDATE channels
SET status = 0
WHERE id NOT IN (
${graphChannelsIds.map(id => `"${id}"`).join(',')}
)
AND status != 2
`);
if (result[0].changedRows ?? 0 > 0) {
logger.info(`Marked ${result[0].changedRows} channels as inactive because they are not in the graph`);
} else {
logger.debug(`Marked ${result[0].changedRows} channels as inactive because they are not in the graph`);
}
} catch (e) {
logger.err('$setChannelsInactive() error: ' + (e instanceof Error ? e.message : e));
}
}
public async $getLatestChannelUpdateForNode(publicKey: string): Promise<number> {
try {
const query = `
SELECT MAX(UNIX_TIMESTAMP(updated_at)) as updated_at
FROM channels
WHERE node1_public_key = ?
`;
const [rows]: any[] = await DB.query(query, [publicKey]);
if (rows.length > 0) {
return rows[0].updated_at;
}
} catch (e) {
logger.err(`Can't getLatestChannelUpdateForNode for ${publicKey}. Reason ${e instanceof Error ? e.message : e}`);
}
return 0;
}
}
export default new ChannelsApi();

View File

@@ -0,0 +1,126 @@
import config from '../../config';
import { Application, Request, Response } from 'express';
import channelsApi from './channels.api';
class ChannelsRoutes {
constructor() { }
public initRoutes(app: Application) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/txids', this.$getChannelsByTransactionIds)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/search/:search', this.$searchChannelsById)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/:short_id', this.$getChannel)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels', this.$getChannelsForNode)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels-geo', this.$getAllChannelsGeo)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels-geo/:publicKey', this.$getAllChannelsGeo)
;
}
private async $searchChannelsById(req: Request, res: Response) {
try {
const channels = await channelsApi.$searchChannelsById(req.params.search);
res.json(channels);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getChannel(req: Request, res: Response) {
try {
const channel = await channelsApi.$getChannel(req.params.short_id);
if (!channel) {
res.status(404).send('Channel not found');
return;
}
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channel);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getChannelsForNode(req: Request, res: Response) {
try {
if (typeof req.query.public_key !== 'string') {
res.status(400).send('Missing parameter: public_key');
return;
}
const index = parseInt(typeof req.query.index === 'string' ? req.query.index : '0', 10) || 0;
const status: string = typeof req.query.status === 'string' ? req.query.status : '';
if (index < -1) {
res.status(400).send('Invalid index');
}
if (['open', 'active', 'closed'].includes(status) === false) {
res.status(400).send('Invalid status');
}
const channels = await channelsApi.$getChannelsForNode(req.query.public_key, index, 10, status);
const channelsCount = await channelsApi.$getChannelsCountForNode(req.query.public_key, status);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.header('X-Total-Count', channelsCount.toString());
res.json(channels);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
try {
if (!Array.isArray(req.query.txId)) {
res.status(400).send('Not an array');
return;
}
const txIds: string[] = [];
for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') {
txIds.push(req.query.txId[_txId].toString());
}
}
const channels = await channelsApi.$getChannelsByTransactionId(txIds);
const result: any[] = [];
for (const txid of txIds) {
const inputs: any = {};
const outputs: any = {};
// Assuming that we only have one lightning close input in each transaction. This may not be true in the future
const foundChannelsFromInput = channels.find((channel) => channel.closing_transaction_id === txid);
if (foundChannelsFromInput) {
inputs[0] = foundChannelsFromInput;
}
const foundChannelsFromOutputs = channels.filter((channel) => channel.transaction_id === txid);
for (const output of foundChannelsFromOutputs) {
outputs[output.transaction_vout] = output;
}
result.push({
inputs,
outputs,
});
}
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getAllChannelsGeo(req: Request, res: Response) {
try {
const style: string = typeof req.query.style === 'string' ? req.query.style : '';
const channels = await channelsApi.$getAllChannelsGeo(req.params?.publicKey, style);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
export default new ChannelsRoutes();

View File

@@ -0,0 +1,58 @@
import config from '../../config';
import { Application, Request, Response } from 'express';
import nodesApi from './nodes.api';
import channelsApi from './channels.api';
import statisticsApi from './statistics.api';
class GeneralLightningRoutes {
constructor() { }
public initRoutes(app: Application) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/search', this.$searchNodesAndChannels)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/statistics/latest', this.$getGeneralStats)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/statistics/:interval', this.$getStatistics)
;
}
private async $searchNodesAndChannels(req: Request, res: Response) {
if (typeof req.query.searchText !== 'string') {
res.status(400).send('Missing parameter: searchText');
return;
}
try {
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.query.searchText);
const channels = await channelsApi.$searchChannelsById(req.query.searchText);
res.json({
nodes: nodes,
channels: channels,
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getStatistics(req: Request, res: Response) {
try {
const statistics = await statisticsApi.$getStatistics(req.params.interval);
const statisticsCount = await statisticsApi.$getStatisticsCount();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', statisticsCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getGeneralStats(req: Request, res: Response) {
try {
const statistics = await statisticsApi.$getLatestStatistics();
res.json(statistics);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
export default new GeneralLightningRoutes();

View File

@@ -0,0 +1,605 @@
import logger from '../../logger';
import DB from '../../database';
import { ResultSetHeader } from 'mysql2';
import { ILightningApi } from '../lightning/lightning-api.interface';
import { ITopNodesPerCapacity, ITopNodesPerChannels } from '../../mempool.interfaces';
class NodesApi {
public async $getWorldNodes(): Promise<any> {
try {
let query = `
SELECT nodes.public_key as publicKey, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias,
CAST(COALESCE(nodes.capacity, 0) as INT) as capacity,
CAST(COALESCE(nodes.channels, 0) as INT) as channels,
nodes.longitude, nodes.latitude,
geo_names_country.names as country, geo_names_iso.names as isoCode
FROM nodes
LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
WHERE status = 1 AND nodes.as_number IS NOT NULL
ORDER BY capacity
`;
const [nodes]: any[] = await DB.query(query);
for (let i = 0; i < nodes.length; ++i) {
nodes[i].country = JSON.parse(nodes[i].country);
}
query = `
SELECT MAX(nodes.capacity) as maxLiquidity, MAX(nodes.channels) as maxChannels
FROM nodes
WHERE status = 1 AND nodes.as_number IS NOT NULL
`;
const [maximums]: any[] = await DB.query(query);
return {
maxLiquidity: maximums[0].maxLiquidity,
maxChannels: maximums[0].maxChannels,
nodes: nodes.map(node => [
node.longitude, node.latitude,
node.publicKey, node.alias, node.capacity, node.channels,
node.country, node.isoCode
])
};
} catch (e) {
logger.err(`Can't get world nodes list. Reason: ${e instanceof Error ? e.message : e}`);
}
}
public async $getNode(public_key: string): Promise<any> {
try {
// General info
let query = `
SELECT public_key, alias, UNIX_TIMESTAMP(first_seen) AS first_seen,
UNIX_TIMESTAMP(updated_at) AS updated_at, color, sockets as sockets,
as_number, city_id, country_id, subdivision_id, longitude, latitude,
geo_names_iso.names as iso_code, geo_names_as.names as as_organization, geo_names_city.names as city,
geo_names_country.names as country, geo_names_subdivision.names as subdivision
FROM nodes
LEFT JOIN geo_names geo_names_as on geo_names_as.id = as_number
LEFT JOIN geo_names geo_names_city on geo_names_city.id = city_id
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = subdivision_id
LEFT JOIN geo_names geo_names_country on geo_names_country.id = country_id
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
WHERE public_key = ?
`;
let [rows]: any[] = await DB.query(query, [public_key]);
if (rows.length === 0) {
throw new Error(`This node does not exist, or our node is not seeing it yet`);
}
const node = rows[0];
node.as_organization = JSON.parse(node.as_organization);
node.subdivision = JSON.parse(node.subdivision);
node.city = JSON.parse(node.city);
node.country = JSON.parse(node.country);
// Active channels and capacity
const activeChannelsStats: any = await this.$getActiveChannelsStats(public_key);
node.active_channel_count = activeChannelsStats.active_channel_count ?? 0;
node.capacity = activeChannelsStats.capacity ?? 0;
// Opened channels count
query = `
SELECT count(short_id) as opened_channel_count
FROM channels
WHERE status != 2 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)
`;
[rows] = await DB.query(query, [public_key, public_key]);
node.opened_channel_count = 0;
if (rows.length > 0) {
node.opened_channel_count = rows[0].opened_channel_count;
}
// Closed channels count
query = `
SELECT count(short_id) as closed_channel_count
FROM channels
WHERE status = 2 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)
`;
[rows] = await DB.query(query, [public_key, public_key]);
node.closed_channel_count = 0;
if (rows.length > 0) {
node.closed_channel_count = rows[0].closed_channel_count;
}
return node;
} catch (e) {
logger.err(`Cannot get node information for ${public_key}. Reason: ${(e instanceof Error ? e.message : e)}`);
throw e;
}
}
public async $getActiveChannelsStats(node_public_key: string): Promise<unknown> {
const query = `
SELECT count(short_id) as active_channel_count, sum(capacity) as capacity
FROM channels
WHERE status = 1 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)
`;
const [rows]: any[] = await DB.query(query, [node_public_key, node_public_key]);
if (rows.length > 0) {
return {
active_channel_count: rows[0].active_channel_count,
capacity: rows[0].capacity
};
} else {
return null;
}
}
public async $getAllNodes(): Promise<any> {
try {
const query = `SELECT * FROM nodes`;
const [rows]: any = await DB.query(query);
return rows;
} catch (e) {
logger.err('$getAllNodes error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getNodeStats(public_key: string): Promise<any> {
try {
const query = `
SELECT UNIX_TIMESTAMP(added) AS added, capacity, channels
FROM node_stats
WHERE public_key = ?
ORDER BY added DESC
`;
const [rows]: any = await DB.query(query, [public_key]);
return rows;
} catch (e) {
logger.err('$getNodeStats error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getTopCapacityNodes(full: boolean): Promise<ITopNodesPerCapacity[]> {
try {
let rows: any;
let query: string;
if (full === false) {
query = `
SELECT nodes.public_key AS publicKey, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias,
nodes.capacity
FROM nodes
ORDER BY capacity DESC
LIMIT 100
`;
[rows] = await DB.query(query);
} else {
query = `
SELECT nodes.public_key AS publicKey, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias,
CAST(COALESCE(nodes.capacity, 0) as INT) as capacity,
CAST(COALESCE(nodes.channels, 0) as INT) as channels,
UNIX_TIMESTAMP(nodes.first_seen) as firstSeen, UNIX_TIMESTAMP(nodes.updated_at) as updatedAt,
geo_names_city.names as city, geo_names_country.names as country,
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
FROM nodes
LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
ORDER BY capacity DESC
LIMIT 100
`;
[rows] = await DB.query(query);
for (let i = 0; i < rows.length; ++i) {
rows[i].country = JSON.parse(rows[i].country);
rows[i].city = JSON.parse(rows[i].city);
}
}
return rows;
} catch (e) {
logger.err('$getTopCapacityNodes error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getTopChannelsNodes(full: boolean): Promise<ITopNodesPerChannels[]> {
try {
let rows: any;
let query: string;
if (full === false) {
query = `
SELECT nodes.public_key as publicKey, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias,
nodes.channels
FROM nodes
ORDER BY channels DESC
LIMIT 100;
`;
[rows] = await DB.query(query);
} else {
query = `
SELECT nodes.public_key AS publicKey, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias,
CAST(COALESCE(nodes.channels, 0) as INT) as channels,
CAST(COALESCE(nodes.capacity, 0) as INT) as capacity,
UNIX_TIMESTAMP(nodes.first_seen) as firstSeen, UNIX_TIMESTAMP(nodes.updated_at) as updatedAt,
geo_names_city.names as city, geo_names_country.names as country,
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
FROM nodes
LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
ORDER BY channels DESC
LIMIT 100
`;
[rows] = await DB.query(query);
for (let i = 0; i < rows.length; ++i) {
rows[i].country = JSON.parse(rows[i].country);
rows[i].city = JSON.parse(rows[i].city);
}
}
return rows;
} catch (e) {
logger.err('$getTopChannelsNodes error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getOldestNodes(full: boolean): Promise<ITopNodesPerChannels[]> {
try {
let [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(MAX(added)) as maxAdded FROM node_stats');
const latestDate = rows[0].maxAdded;
let query: string;
if (full === false) {
query = `
SELECT nodes.public_key, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias,
node_stats.channels
FROM node_stats
JOIN nodes ON nodes.public_key = node_stats.public_key
WHERE added = FROM_UNIXTIME(${latestDate})
ORDER BY first_seen
LIMIT 100;
`;
[rows] = await DB.query(query);
} else {
query = `
SELECT node_stats.public_key AS publicKey, IF(nodes.alias = '', SUBSTRING(node_stats.public_key, 1, 20), alias) as alias,
CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity,
UNIX_TIMESTAMP(nodes.first_seen) as firstSeen, UNIX_TIMESTAMP(nodes.updated_at) as updatedAt,
geo_names_city.names as city, geo_names_country.names as country,
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
FROM node_stats
RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key
LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
WHERE added = FROM_UNIXTIME(${latestDate})
ORDER BY first_seen
LIMIT 100
`;
[rows] = await DB.query(query);
for (let i = 0; i < rows.length; ++i) {
rows[i].country = JSON.parse(rows[i].country);
rows[i].city = JSON.parse(rows[i].city);
}
}
return rows;
} catch (e) {
logger.err('$getTopChannelsNodes error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $searchNodeByPublicKeyOrAlias(search: string) {
try {
const publicKeySearch = search.replace('%', '') + '%';
const aliasSearch = search.replace(/[-_.]/g, ' ').replace(/[^a-zA-Z0-9 ]/g, '').split(' ').map((search) => '+' + search + '*').join(' ');
const query = `SELECT public_key, alias, capacity, channels, status FROM nodes WHERE public_key LIKE ? OR MATCH alias_search AGAINST (? IN BOOLEAN MODE) ORDER BY capacity DESC LIMIT 10`;
const [rows]: any = await DB.query(query, [publicKeySearch, aliasSearch]);
return rows;
} catch (e) {
logger.err('$searchNodeByPublicKeyOrAlias error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getNodesISPRanking() {
try {
let query = '';
// List all channels and the two linked ISP
query = `
SELECT short_id, channels.capacity,
channels.node1_public_key AS node1PublicKey, isp1.names AS isp1, isp1.id as isp1ID,
channels.node2_public_key AS node2PublicKey, isp2.names AS isp2, isp2.id as isp2ID
FROM channels
JOIN nodes node1 ON node1.public_key = channels.node1_public_key
JOIN nodes node2 ON node2.public_key = channels.node2_public_key
JOIN geo_names isp1 ON isp1.id = node1.as_number
JOIN geo_names isp2 ON isp2.id = node2.as_number
WHERE channels.status = 1
ORDER BY short_id DESC
`;
const [channelsIsp]: any = await DB.query(query);
// Sum channels capacity and node count per ISP
const ispList = {};
for (const channel of channelsIsp) {
const isp1 = JSON.parse(channel.isp1);
const isp2 = JSON.parse(channel.isp2);
if (!ispList[isp1]) {
ispList[isp1] = {
id: channel.isp1ID.toString(),
capacity: 0,
channels: 0,
nodes: {},
};
} else if (ispList[isp1].id.indexOf(channel.isp1ID) === -1) {
ispList[isp1].id += ',' + channel.isp1ID.toString();
}
if (!ispList[isp2]) {
ispList[isp2] = {
id: channel.isp2ID.toString(),
capacity: 0,
channels: 0,
nodes: {},
};
} else if (ispList[isp2].id.indexOf(channel.isp2ID) === -1) {
ispList[isp2].id += ',' + channel.isp2ID.toString();
}
ispList[isp1].capacity += channel.capacity;
ispList[isp1].channels += 1;
ispList[isp1].nodes[channel.node1PublicKey] = true;
ispList[isp2].capacity += channel.capacity;
ispList[isp2].channels += 1;
ispList[isp2].nodes[channel.node2PublicKey] = true;
}
const ispRanking: any[] = [];
for (const isp of Object.keys(ispList)) {
ispRanking.push([
ispList[isp].id,
isp,
ispList[isp].capacity,
ispList[isp].channels,
Object.keys(ispList[isp].nodes).length,
]);
}
// Total active channels capacity
query = `SELECT SUM(capacity) AS capacity FROM channels WHERE status = 1`;
const [totalCapacity]: any = await DB.query(query);
// Get the total capacity of all channels which have at least one node on clearnet
query = `
SELECT SUM(capacity) as capacity
FROM (
SELECT capacity, GROUP_CONCAT(socket1.type, socket2.type) as networks
FROM channels
JOIN nodes_sockets socket1 ON node1_public_key = socket1.public_key
JOIN nodes_sockets socket2 ON node2_public_key = socket2.public_key
AND channels.status = 1
GROUP BY short_id
) channels_tmp
WHERE channels_tmp.networks LIKE '%ipv%'
`;
const [clearnetCapacity]: any = await DB.query(query);
// Get the total capacity of all channels which have both nodes on Tor
query = `
SELECT SUM(capacity) as capacity
FROM (
SELECT capacity, GROUP_CONCAT(socket1.type, socket2.type) as networks
FROM channels
JOIN nodes_sockets socket1 ON node1_public_key = socket1.public_key
JOIN nodes_sockets socket2 ON node2_public_key = socket2.public_key
AND channels.status = 1
GROUP BY short_id
) channels_tmp
WHERE channels_tmp.networks NOT LIKE '%ipv%' AND
channels_tmp.networks NOT LIKE '%dns%' AND
channels_tmp.networks NOT LIKE '%websocket%'
`;
const [torCapacity]: any = await DB.query(query);
const clearnetCapacityValue = parseInt(clearnetCapacity[0].capacity, 10);
const torCapacityValue = parseInt(torCapacity[0].capacity, 10);
const unknownCapacityValue = parseInt(totalCapacity[0].capacity) - clearnetCapacityValue - torCapacityValue;
return {
clearnetCapacity: clearnetCapacityValue,
torCapacity: torCapacityValue,
unknownCapacity: unknownCapacityValue,
ispRanking: ispRanking,
};
} catch (e) {
logger.err(`Cannot get LN ISP ranking. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
}
}
public async $getNodesPerCountry(countryId: string) {
try {
const query = `
SELECT nodes.public_key, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.channels, 0) as INT) as channels,
nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
geo_names_city.names as city, geo_names_country.names as country,
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision,
nodes.longitude, nodes.latitude, nodes.as_number, geo_names_isp.names as isp
FROM nodes
LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
LEFT JOIN geo_names geo_names_isp on geo_names_isp.id = nodes.as_number AND geo_names_isp.type = 'as_organization'
WHERE geo_names_country.id = ?
ORDER BY capacity DESC
`;
const [rows]: any = await DB.query(query, [countryId]);
for (let i = 0; i < rows.length; ++i) {
rows[i].country = JSON.parse(rows[i].country);
rows[i].city = JSON.parse(rows[i].city);
rows[i].subdivision = JSON.parse(rows[i].subdivision);
rows[i].isp = JSON.parse(rows[i].isp);
}
return rows;
} catch (e) {
logger.err(`Cannot get nodes for country id ${countryId}. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
}
}
public async $getNodesPerISP(ISPId: string) {
try {
const query = `
SELECT nodes.public_key, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.channels, 0) as INT) as channels,
nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
geo_names_city.names as city, geo_names_country.names as country,
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision,
nodes.longitude, nodes.latitude
FROM nodes
LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
WHERE nodes.as_number IN (?)
ORDER BY capacity DESC
`;
const [rows]: any = await DB.query(query, [ISPId.split(',')]);
for (let i = 0; i < rows.length; ++i) {
rows[i].country = JSON.parse(rows[i].country);
rows[i].city = JSON.parse(rows[i].city);
rows[i].subdivision = JSON.parse(rows[i].subdivision);
}
return rows;
} catch (e) {
logger.err(`Cannot get nodes for ISP id ${ISPId}. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
}
}
public async $getNodesCountries() {
try {
let query = `SELECT geo_names.names as names, geo_names_iso.names as iso_code, COUNT(DISTINCT nodes.public_key) as nodesCount, SUM(capacity) as capacity
FROM nodes
JOIN geo_names ON geo_names.id = nodes.country_id AND geo_names.type = 'country'
JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
GROUP BY country_id
ORDER BY COUNT(DISTINCT nodes.public_key) DESC
`;
const [nodesCountPerCountry]: any = await DB.query(query);
query = `SELECT COUNT(*) as total FROM nodes WHERE country_id IS NOT NULL`;
const [nodesWithAS]: any = await DB.query(query);
const nodesPerCountry: any[] = [];
for (const country of nodesCountPerCountry) {
nodesPerCountry.push({
name: JSON.parse(country.names),
iso: country.iso_code,
count: country.nodesCount,
share: Math.floor(country.nodesCount / nodesWithAS[0].total * 10000) / 100,
capacity: country.capacity,
})
}
return nodesPerCountry;
} catch (e) {
logger.err(`Cannot get nodes grouped by AS. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
}
}
/**
* Save or update a node present in the graph
*/
public async $saveNode(node: ILightningApi.Node): Promise<void> {
try {
const sockets = (node.addresses?.map(a => a.addr).join(',')) ?? '';
const query = `INSERT INTO nodes(
public_key,
first_seen,
updated_at,
alias,
alias_search,
color,
sockets,
status
)
VALUES (?, NOW(), FROM_UNIXTIME(?), ?, ?, ?, ?, 1)
ON DUPLICATE KEY UPDATE updated_at = FROM_UNIXTIME(?), alias = ?, alias_search = ?, color = ?, sockets = ?, status = 1`;
await DB.query(query, [
node.pub_key,
node.last_update,
node.alias,
this.aliasToSearchText(node.alias),
node.color,
sockets,
node.last_update,
node.alias,
this.aliasToSearchText(node.alias),
node.color,
sockets,
]);
} catch (e) {
logger.err('$saveNode() error: ' + (e instanceof Error ? e.message : e));
}
}
/**
* Update node sockets
*/
public async $updateNodeSockets(publicKey: string, sockets: {network: string; addr: string}[]): Promise<void> {
const formattedSockets = (sockets.map(a => a.addr).join(',')) ?? '';
try {
await DB.query(`UPDATE nodes SET sockets = ? WHERE public_key = ?`, [formattedSockets, publicKey]);
} catch (e) {
logger.err(`Cannot update node sockets for ${publicKey}. Reason: ${e instanceof Error ? e.message : e}`);
}
}
/**
* Set all nodes not in `nodesPubkeys` as inactive (status = 0)
*/
public async $setNodesInactive(graphNodesPubkeys: string[]): Promise<void> {
if (graphNodesPubkeys.length === 0) {
return;
}
try {
const result = await DB.query<ResultSetHeader>(`
UPDATE nodes
SET status = 0
WHERE public_key NOT IN (
${graphNodesPubkeys.map(pubkey => `"${pubkey}"`).join(',')}
)
`);
if (result[0].changedRows ?? 0 > 0) {
logger.info(`Marked ${result[0].changedRows} nodes as inactive because they are not in the graph`);
} else {
logger.debug(`Marked ${result[0].changedRows} nodes as inactive because they are not in the graph`);
}
} catch (e) {
logger.err('$setNodesInactive() error: ' + (e instanceof Error ? e.message : e));
}
}
private aliasToSearchText(str: string): string {
return str.replace(/[-_.]/g, ' ').replace(/[^a-zA-Z0-9 ]/g, '');
}
}
export default new NodesApi();

View File

@@ -0,0 +1,242 @@
import config from '../../config';
import { Application, Request, Response } from 'express';
import nodesApi from './nodes.api';
import DB from '../../database';
import { INodesRanking } from '../../mempool.interfaces';
class NodesRoutes {
constructor() { }
public initRoutes(app: Application) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/world', this.$getWorldNodes)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/country/:country', this.$getNodesPerCountry)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/search/:search', this.$searchNode)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp-ranking', this.$getISPRanking)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp/:isp', this.$getNodesPerISP)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/countries', this.$getNodesCountries)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings', this.$getNodesRanking)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/liquidity', this.$getTopNodesByCapacity)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/connectivity', this.$getTopNodesByChannels)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/age', this.$getOldestNodes)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/group/:name', this.$getNodeGroup)
;
}
private async $searchNode(req: Request, res: Response) {
try {
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
res.json(nodes);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getNodeGroup(req: Request, res: Response) {
try {
let nodesList;
let nodes: any[] = [];
switch (config.MEMPOOL.NETWORK) {
case 'testnet':
nodesList = ['032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b', '025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7', '0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55', '032ab2028c0b614c6d87824e2373529652fd7e4221b4c70cc4da7c7005c49afcf0', '029001b22fe70b48bee12d014df91982eb85ff1bd404ec772d5c83c4ee3e88d2c3', '0212e2848d79f928411da5f2ff0a8c95ec6ccb5a09d2031b6f71e91309dcde63af', '03e871a2229523d34f76e6311ff197cfe7f26c2fbec13554b93a46f4e710c47dab', '032202ec98d976b0e928bd1d91924e8bd3eab07231fc39feb3737b010071073df8', '02fa7c5a948d03d563a9f36940c2205a814e594d17c0042ced242c71a857d72605', '039c14fdec2d958e3d14cebf657451bbd9e039196615785e82c917f274e3fb2205', '033589bbcb233ffc416cefd5437c7f37e9d7cb7942d405e39e72c4c846d9b37f18', '029293110441c6e2eacb57e1255bf6ef05c41a6a676fe474922d33c19f98a7d584'];
break;
case 'signet':
nodesList = ['03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956', '033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de', '02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781', '025196512905b8a3f1597428b867bec63ec9a95e5089eb7dc7e63e2d2691669029', '027c625aa1fbe3768db68ebcb05b53b6dc0ce68b7b54b8900d326d167363e684fe', '03f1629af3101fcc56b7aac2667016be84e3defbf3d0c8719f836c9b41c9a57a43', '02dfb81e2f7a3c4c9e8a51b70ef82b4a24549cc2fab1f5b2fd636501774a918991', '02d01ccf832944c68f10d39006093769c5b8bda886d561b128534e313d729fdb34', '02499ed23027d4698a6904ff4ec1b6085a61f10b9a6937f90438f9947e38e8ea86', '038310e3a786340f2bd7770704c7ccfe560fd163d9a1c99d67894597419d12cbf7', '03e5e9d879b72c7d67ecd483bae023bd33e695bb32b981a4021260f7b9d62bc761', '028d16e1a0ace4c0c0a421536d8d32ce484dfe6e2f726b7b0e7c30f12a195f8cc7'];
break;
default:
nodesList = ['03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61', '03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437', '03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144', '0238bd27f02d67d6c51e269692bc8c9a32357a00e7777cba7f4f1f18a2a700b108', '03f983dcabed6baa1eab5b56c8b2e8fdc846ab3fd931155377897335e85a9fa57c', '03e399589533581e48796e29a825839a010036a61b20744fda929d6709fcbffcc5', '021f5288b5f72c42cd0d8801086af7ce09a816d8ee9a4c47a4b436399b26cb601a', '032b01b7585f781420cd4148841a82831ba37fa952342052cec16750852d4f2dd9', '02848036488d4b8fb1f1c4064261ec36151f43b085f0b51bd239ade3ddfc940c34', '02b6b1640fe029e304c216951af9fbefdb23b0bdc9baaf327540d31b6107841fdf', '03694289827203a5b3156d753071ddd5bf92e371f5a462943f9555eef6d2d6606c', '0283d850db7c3e8ea7cc9c4abc7afaab12bbdf72b677dcba1d608350d2537d7d43'];
}
for (let pubKey of nodesList) {
try {
const node = await nodesApi.$getNode(pubKey);
if (node) {
nodes.push(node);
}
} catch (e) {}
}
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(nodes);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getNode(req: Request, res: Response) {
try {
const node = await nodesApi.$getNode(req.params.public_key);
if (!node) {
res.status(404).send('Node not found');
return;
}
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getHistoricalNodeStats(req: Request, res: Response) {
try {
const statistics = await nodesApi.$getNodeStats(req.params.public_key);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getNodesRanking(req: Request, res: Response): Promise<void> {
try {
const topCapacityNodes = await nodesApi.$getTopCapacityNodes(false);
const topChannelsNodes = await nodesApi.$getTopChannelsNodes(false);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(<INodesRanking>{
topByCapacity: topCapacityNodes,
topByChannels: topChannelsNodes,
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getTopNodesByCapacity(req: Request, res: Response): Promise<void> {
try {
const topCapacityNodes = await nodesApi.$getTopCapacityNodes(true);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getTopNodesByChannels(req: Request, res: Response): Promise<void> {
try {
const topCapacityNodes = await nodesApi.$getTopChannelsNodes(true);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getOldestNodes(req: Request, res: Response): Promise<void> {
try {
const topCapacityNodes = await nodesApi.$getOldestNodes(true);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getISPRanking(req: Request, res: Response): Promise<void> {
try {
const nodesPerAs = await nodesApi.$getNodesISPRanking();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getWorldNodes(req: Request, res: Response) {
try {
const worldNodes = await nodesApi.$getWorldNodes();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(worldNodes);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getNodesPerCountry(req: Request, res: Response) {
try {
const [country]: any[] = await DB.query(
`SELECT geo_names.id, geo_names_country.names as country_names
FROM geo_names
JOIN geo_names geo_names_country on geo_names.id = geo_names_country.id AND geo_names_country.type = 'country'
WHERE geo_names.type = 'country_iso_code' AND geo_names.names = ?`,
[req.params.country]
);
if (country.length === 0) {
res.status(404).send(`This country does not exist or does not host any lightning nodes on clearnet`);
return;
}
const nodes = await nodesApi.$getNodesPerCountry(country[0].id);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json({
country: JSON.parse(country[0].country_names),
nodes: nodes,
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getNodesPerISP(req: Request, res: Response) {
try {
const [isp]: any[] = await DB.query(
`SELECT geo_names.names as isp_name
FROM geo_names
WHERE geo_names.type = 'as_organization' AND geo_names.id = ?`,
[req.params.isp]
);
if (isp.length === 0) {
res.status(404).send(`This ISP does not exist or does not host any lightning nodes on clearnet`);
return;
}
const nodes = await nodesApi.$getNodesPerISP(req.params.isp);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json({
isp: JSON.parse(isp[0].isp_name),
nodes: nodes,
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getNodesCountries(req: Request, res: Response) {
try {
const nodesPerAs = await nodesApi.$getNodesCountries();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
export default new NodesRoutes();

View File

@@ -0,0 +1,53 @@
import logger from '../../logger';
import DB from '../../database';
import { Common } from '../common';
class StatisticsApi {
public async $getStatistics(interval: string | null = null): Promise<any> {
interval = Common.getSqlInterval(interval);
let query = `SELECT UNIX_TIMESTAMP(added) AS added, channel_count, total_capacity,
tor_nodes, clearnet_nodes, unannounced_nodes, clearnet_tor_nodes
FROM lightning_stats`;
if (interval) {
query += ` WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
query += ` ORDER BY added DESC`;
try {
const [rows]: any = await DB.query(query);
return rows;
} catch (e) {
logger.err('$getStatistics error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getLatestStatistics(): Promise<any> {
try {
const [rows]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY added DESC LIMIT 1`);
const [rows2]: any = await DB.query(`SELECT * FROM lightning_stats WHERE DATE(added) = DATE(NOW() - INTERVAL 7 DAY)`);
return {
latest: rows[0],
previous: rows2[0],
};
} catch (e) {
logger.err('$getLatestStatistics error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getStatisticsCount(): Promise<number> {
try {
const [rows]: any = await DB.query(`SELECT count(*) as count FROM lightning_stats`);
return rows[0].count;
} catch (e) {
logger.err('$getLatestStatistics error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
}
export default new StatisticsApi();

View File

@@ -0,0 +1,37 @@
import fs from 'fs';
import path from "path";
const { spawnSync } = require('child_process');
function getVersion(): string {
const packageJson = fs.readFileSync('package.json').toString();
return JSON.parse(packageJson).version;
}
function getGitCommit(): string {
if (process.env.MEMPOOL_COMMIT_HASH) {
return process.env.MEMPOOL_COMMIT_HASH;
} else {
const gitRevParse = spawnSync('git', ['rev-parse', '--short', 'HEAD']);
if (!gitRevParse.error) {
const output = gitRevParse.stdout.toString('utf-8').replace(/[\n\r\s]+$/, '');
if (output) {
return output;
} else {
console.log('Could not fetch git commit: No repo available');
}
} else if (gitRevParse.error.code === 'ENOENT') {
console.log('Could not fetch git commit: Command `git` is unavailable');
}
}
return '?';
}
const versionInfo = {
version: getVersion(),
gitCommit: getGitCommit()
}
fs.writeFileSync(
path.join(__dirname, 'version.json'),
JSON.stringify(versionInfo, null, 2) + "\n"
);

View File

@@ -0,0 +1,272 @@
// Imported from https://github.com/shesek/lightning-client-js
'use strict';
const methods = [
'addgossip',
'autocleaninvoice',
'check',
'checkmessage',
'close',
'connect',
'createinvoice',
'createinvoicerequest',
'createoffer',
'createonion',
'decode',
'decodepay',
'delexpiredinvoice',
'delinvoice',
'delpay',
'dev-listaddrs',
'dev-rescan-outputs',
'disableoffer',
'disconnect',
'estimatefees',
'feerates',
'fetchinvoice',
'fundchannel',
'fundchannel_cancel',
'fundchannel_complete',
'fundchannel_start',
'fundpsbt',
'getchaininfo',
'getinfo',
'getlog',
'getrawblockbyheight',
'getroute',
'getsharedsecret',
'getutxout',
'help',
'invoice',
'keysend',
'legacypay',
'listchannels',
'listconfigs',
'listforwards',
'listfunds',
'listinvoices',
'listnodes',
'listoffers',
'listpays',
'listpeers',
'listsendpays',
'listtransactions',
'multifundchannel',
'multiwithdraw',
'newaddr',
'notifications',
'offer',
'offerout',
'openchannel_abort',
'openchannel_bump',
'openchannel_init',
'openchannel_signed',
'openchannel_update',
'pay',
'payersign',
'paystatus',
'ping',
'plugin',
'reserveinputs',
'sendinvoice',
'sendonion',
'sendonionmessage',
'sendpay',
'sendpsbt',
'sendrawtransaction',
'setchannelfee',
'signmessage',
'signpsbt',
'stop',
'txdiscard',
'txprepare',
'txsend',
'unreserveinputs',
'utxopsbt',
'waitanyinvoice',
'waitblockheight',
'waitinvoice',
'waitsendpay',
'withdraw'
];
import EventEmitter from 'events';
import { existsSync, statSync } from 'fs';
import { createConnection, Socket } from 'net';
import { homedir } from 'os';
import path from 'path';
import { createInterface, Interface } from 'readline';
import logger from '../../../logger';
import { AbstractLightningApi } from '../lightning-api-abstract-factory';
import { ILightningApi } from '../lightning-api.interface';
import { convertAndmergeBidirectionalChannels, convertNode } from './clightning-convert';
class LightningError extends Error {
type: string = 'lightning';
message: string = 'lightning-client error';
constructor(error) {
super();
this.type = error.type;
this.message = error.message;
}
}
const defaultRpcPath = path.join(homedir(), '.lightning')
, fStat = (...p) => statSync(path.join(...p))
, fExists = (...p) => existsSync(path.join(...p))
export default class CLightningClient extends EventEmitter implements AbstractLightningApi {
private rpcPath: string;
private reconnectWait: number;
private reconnectTimeout;
private reqcount: number;
private client: Socket;
private rl: Interface;
private clientConnectionPromise: Promise<unknown>;
constructor(rpcPath = defaultRpcPath) {
if (!path.isAbsolute(rpcPath)) {
throw new Error('The rpcPath must be an absolute path');
}
if (!fExists(rpcPath) || !fStat(rpcPath).isSocket()) {
// network directory provided, use the lightning-rpc within in
if (fExists(rpcPath, 'lightning-rpc')) {
rpcPath = path.join(rpcPath, 'lightning-rpc');
}
// main data directory provided, default to using the bitcoin mainnet subdirectory
// to be removed in v0.2.0
else if (fExists(rpcPath, 'bitcoin', 'lightning-rpc')) {
logger.warn(`[CLightningClient] ${rpcPath}/lightning-rpc is missing, using the bitcoin mainnet subdirectory at ${rpcPath}/bitcoin instead.`)
logger.warn(`[CLightningClient] specifying the main lightning data directory is deprecated, please specify the network directory explicitly.\n`)
rpcPath = path.join(rpcPath, 'bitcoin', 'lightning-rpc')
}
}
logger.debug(`[CLightningClient] Connecting to ${rpcPath}`);
super();
this.rpcPath = rpcPath;
this.reconnectWait = 0.5;
this.reconnectTimeout = null;
this.reqcount = 0;
const _self = this;
this.client = createConnection(rpcPath).on(
'error', () => {
_self.increaseWaitTime();
_self.reconnect();
}
);
this.rl = createInterface({ input: this.client }).on(
'error', () => {
_self.increaseWaitTime();
_self.reconnect();
}
);
this.clientConnectionPromise = new Promise<void>(resolve => {
_self.client.on('connect', () => {
logger.info(`[CLightningClient] Lightning client connected`);
_self.reconnectWait = 1;
resolve();
});
_self.client.on('end', () => {
logger.err('[CLightningClient] Lightning client connection closed, reconnecting');
_self.increaseWaitTime();
_self.reconnect();
});
_self.client.on('error', error => {
logger.err(`[CLightningClient] Lightning client connection error: ${error}`);
_self.increaseWaitTime();
_self.reconnect();
});
});
this.rl.on('line', line => {
line = line.trim();
if (!line) {
return;
}
const data = JSON.parse(line);
// logger.debug(`[CLightningClient] #${data.id} <-- ${JSON.stringify(data.error || data.result)}`);
_self.emit('res:' + data.id, data);
});
}
increaseWaitTime(): void {
if (this.reconnectWait >= 16) {
this.reconnectWait = 16;
} else {
this.reconnectWait *= 2;
}
}
reconnect(): void {
const _self = this;
if (this.reconnectTimeout) {
return;
}
this.reconnectTimeout = setTimeout(() => {
logger.debug('[CLightningClient] Trying to reconnect...');
_self.client.connect(_self.rpcPath);
_self.reconnectTimeout = null;
}, this.reconnectWait * 1000);
}
call(method, args = []): Promise<any> {
const _self = this;
const callInt = ++this.reqcount;
const sendObj = {
jsonrpc: '2.0',
method,
params: args,
id: '' + callInt
};
// logger.debug(`[CLightningClient] #${callInt} --> ${method} ${args}`);
// Wait for the client to connect
return this.clientConnectionPromise
.then(() => new Promise((resolve, reject) => {
// Wait for a response
this.once('res:' + callInt, res => res.error == null
? resolve(res.result)
: reject(new LightningError(res.error))
);
// Send the command
_self.client.write(JSON.stringify(sendObj));
}));
}
async $getNetworkGraph(): Promise<ILightningApi.NetworkGraph> {
const listnodes: any[] = await this.call('listnodes');
const listchannels: any[] = await this.call('listchannels');
const channelsList = await convertAndmergeBidirectionalChannels(listchannels['channels']);
return {
nodes: listnodes['nodes'].map(node => convertNode(node)),
edges: channelsList,
};
}
}
const protify = s => s.replace(/-([a-z])/g, m => m[1].toUpperCase());
methods.forEach(k => {
CLightningClient.prototype[protify(k)] = function (...args: any) {
return this.call(k, args);
};
});

View File

@@ -0,0 +1,135 @@
import { ILightningApi } from '../lightning-api.interface';
import FundingTxFetcher from '../../../tasks/lightning/sync-tasks/funding-tx-fetcher';
import logger from '../../../logger';
import { Common } from '../../common';
/**
* Convert a clightning "listnode" entry to a lnd node entry
*/
export function convertNode(clNode: any): ILightningApi.Node {
return {
alias: clNode.alias ?? '',
color: `#${clNode.color ?? ''}`,
features: [], // TODO parse and return clNode.feature
pub_key: clNode.nodeid,
addresses: clNode.addresses?.map((addr) => {
let address = addr.address;
if (addr.type === 'ipv6') {
address = `[${address}]`;
}
return {
network: addr.type,
addr: `${address}:${addr.port}`
};
}) ?? [],
last_update: clNode?.last_timestamp ?? 0,
};
}
/**
* Convert clightning "listchannels" response to lnd "describegraph.edges" format
*/
export async function convertAndmergeBidirectionalChannels(clChannels: any[]): Promise<ILightningApi.Channel[]> {
logger.info('Converting clightning nodes and channels to lnd graph format');
let loggerTimer = new Date().getTime() / 1000;
let channelProcessed = 0;
const consolidatedChannelList: ILightningApi.Channel[] = [];
const clChannelsDict = {};
const clChannelsDictCount = {};
for (const clChannel of clChannels) {
if (!clChannelsDict[clChannel.short_channel_id]) {
clChannelsDict[clChannel.short_channel_id] = clChannel;
clChannelsDictCount[clChannel.short_channel_id] = 1;
} else {
consolidatedChannelList.push(
await buildFullChannel(clChannel, clChannelsDict[clChannel.short_channel_id])
);
delete clChannelsDict[clChannel.short_channel_id];
clChannelsDictCount[clChannel.short_channel_id]++;
}
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
if (elapsedSeconds > 10) {
logger.info(`Building complete channels from clightning output. Channels processed: ${channelProcessed + 1} of ${clChannels.length}`);
loggerTimer = new Date().getTime() / 1000;
}
++channelProcessed;
}
channelProcessed = 0;
const keys = Object.keys(clChannelsDict);
for (const short_channel_id of keys) {
consolidatedChannelList.push(await buildIncompleteChannel(clChannelsDict[short_channel_id]));
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
if (elapsedSeconds > 10) {
logger.info(`Building partial channels from clightning output. Channels processed: ${channelProcessed + 1} of ${keys.length}`);
loggerTimer = new Date().getTime() / 1000;
}
}
return consolidatedChannelList;
}
/**
* Convert two clightning "getchannels" entries into a full a lnd "describegraph.edges" format
* In this case, clightning knows the channel policy for both nodes
*/
async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILightningApi.Channel> {
const lastUpdate = Math.max(clChannelA.last_update ?? 0, clChannelB.last_update ?? 0);
const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannelA.short_channel_id);
const parts = clChannelA.short_channel_id.split('x');
const outputIdx = parts[2];
return {
channel_id: Common.channelShortIdToIntegerId(clChannelA.short_channel_id),
capacity: clChannelA.satoshis,
last_update: lastUpdate,
node1_policy: convertPolicy(clChannelA),
node2_policy: convertPolicy(clChannelB),
chan_point: `${tx.txid}:${outputIdx}`,
node1_pub: clChannelA.source,
node2_pub: clChannelB.source,
};
}
/**
* Convert one clightning "getchannels" entry into a full a lnd "describegraph.edges" format
* In this case, clightning knows the channel policy of only one node
*/
async function buildIncompleteChannel(clChannel: any): Promise<ILightningApi.Channel> {
const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannel.short_channel_id);
const parts = clChannel.short_channel_id.split('x');
const outputIdx = parts[2];
return {
channel_id: Common.channelShortIdToIntegerId(clChannel.short_channel_id),
capacity: clChannel.satoshis,
last_update: clChannel.last_update ?? 0,
node1_policy: convertPolicy(clChannel),
node2_policy: null,
chan_point: `${tx.txid}:${outputIdx}`,
node1_pub: clChannel.source,
node2_pub: clChannel.destination,
};
}
/**
* Convert a clightning "listnode" response to a lnd channel policy format
*/
function convertPolicy(clChannel: any): ILightningApi.RoutingPolicy {
return {
time_lock_delta: clChannel.delay,
min_htlc: clChannel.htlc_minimum_msat.slice(0, -4),
max_htlc_msat: clChannel.htlc_maximum_msat.slice(0, -4),
fee_base_msat: clChannel.base_fee_millisatoshi,
fee_rate_milli_msat: clChannel.fee_per_millionth,
disabled: !clChannel.active,
last_update: clChannel.last_update ?? 0,
};
}

View File

@@ -0,0 +1,5 @@
import { ILightningApi } from './lightning-api.interface';
export interface AbstractLightningApi {
$getNetworkGraph(): Promise<ILightningApi.NetworkGraph>;
}

View File

@@ -0,0 +1,16 @@
import config from '../../config';
import CLightningClient from './clightning/clightning-client';
import { AbstractLightningApi } from './lightning-api-abstract-factory';
import LndApi from './lnd/lnd-api';
function lightningApiFactory(): AbstractLightningApi {
switch (config.LIGHTNING.ENABLED === true && config.LIGHTNING.BACKEND) {
case 'cln':
return new CLightningClient(config.CLIGHTNING.SOCKET);
case 'lnd':
default:
return new LndApi();
}
}
export default lightningApiFactory();

View File

@@ -0,0 +1,85 @@
export namespace ILightningApi {
export interface NetworkInfo {
graph_diameter: number;
avg_out_degree: number;
max_out_degree: number;
num_nodes: number;
num_channels: number;
total_network_capacity: string;
avg_channel_size: number;
min_channel_size: string;
max_channel_size: string;
median_channel_size_sat: string;
num_zombie_chans: string;
}
export interface NetworkGraph {
nodes: Node[];
edges: Channel[];
}
export interface Channel {
channel_id: string;
chan_point: string;
last_update: number;
node1_pub: string;
node2_pub: string;
capacity: string;
node1_policy: RoutingPolicy | null;
node2_policy: RoutingPolicy | null;
}
export interface RoutingPolicy {
time_lock_delta: number;
min_htlc: string;
fee_base_msat: string;
fee_rate_milli_msat: string;
disabled: boolean;
max_htlc_msat: string;
last_update: number;
}
export interface Node {
last_update: number;
pub_key: string;
alias: string;
addresses: {
network: string;
addr: string;
}[];
color: string;
features: { [key: number]: Feature };
}
export interface Info {
identity_pubkey: string;
alias: string;
num_pending_channels: number;
num_active_channels: number;
num_peers: number;
block_height: number;
block_hash: string;
synced_to_chain: boolean;
testnet: boolean;
uris: string[];
best_header_timestamp: string;
version: string;
num_inactive_channels: number;
chains: {
chain: string;
network: string;
}[];
color: string;
synced_to_graph: boolean;
features: { [key: number]: Feature };
commit_hash: string;
/** Available on LND since v0.15.0-beta */
require_htlc_interceptor?: boolean;
}
export interface Feature {
name: string;
is_required: boolean;
is_known: boolean;
}
}

View File

@@ -0,0 +1,41 @@
import axios, { AxiosRequestConfig } from 'axios';
import { Agent } from 'https';
import * as fs from 'fs';
import { AbstractLightningApi } from '../lightning-api-abstract-factory';
import { ILightningApi } from '../lightning-api.interface';
import config from '../../../config';
class LndApi implements AbstractLightningApi {
axiosConfig: AxiosRequestConfig = {};
constructor() {
if (config.LIGHTNING.ENABLED) {
this.axiosConfig = {
headers: {
'Grpc-Metadata-macaroon': fs.readFileSync(config.LND.MACAROON_PATH).toString('hex')
},
httpsAgent: new Agent({
ca: fs.readFileSync(config.LND.TLS_CERT_PATH)
}),
timeout: 10000
};
}
}
async $getNetworkInfo(): Promise<ILightningApi.NetworkInfo> {
return axios.get<ILightningApi.NetworkInfo>(config.LND.REST_API_URL + '/v1/graph/info', this.axiosConfig)
.then((response) => response.data);
}
async $getInfo(): Promise<ILightningApi.Info> {
return axios.get<ILightningApi.Info>(config.LND.REST_API_URL + '/v1/getinfo', this.axiosConfig)
.then((response) => response.data);
}
async $getNetworkGraph(): Promise<ILightningApi.NetworkGraph> {
return axios.get<ILightningApi.NetworkGraph>(config.LND.REST_API_URL + '/v1/graph', this.axiosConfig)
.then((response) => response.data);
}
}
export default LndApi;

View File

@@ -0,0 +1,73 @@
import axios from 'axios';
import { Application, Request, Response } from 'express';
import config from '../../config';
import elementsParser from './elements-parser';
import icons from './icons';
class LiquidRoutes {
public initRoutes(app: Application) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'assets/icons', this.getAllLiquidIcon)
.get(config.MEMPOOL.API_URL_PREFIX + 'assets/featured', this.$getAllFeaturedLiquidAssets)
.get(config.MEMPOOL.API_URL_PREFIX + 'asset/:assetId/icon', this.getLiquidIcon)
.get(config.MEMPOOL.API_URL_PREFIX + 'assets/group/:id', this.$getAssetGroup)
;
if (config.DATABASE.ENABLED) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', this.$getElementsPegsByMonth)
;
}
}
private getLiquidIcon(req: Request, res: Response) {
const result = icons.getIconByAssetId(req.params.assetId);
if (result) {
res.setHeader('content-type', 'image/png');
res.setHeader('content-length', result.length);
res.send(result);
} else {
res.status(404).send('Asset icon not found');
}
}
private getAllLiquidIcon(req: Request, res: Response) {
const result = icons.getAllIconIds();
if (result) {
res.json(result);
} else {
res.status(404).send('Asset icons not found');
}
}
private async $getAllFeaturedLiquidAssets(req: Request, res: Response) {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.LIQUID_API}/assets/featured`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
}
private async $getAssetGroup(req: Request, res: Response) {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.LIQUID_API}/assets/group/${parseInt(req.params.id, 10)}`,
{ responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
}
private async $getElementsPegsByMonth(req: Request, res: Response) {
try {
const pegs = await elementsParser.$getPegDataByMonth();
res.json(pegs);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
export default new LiquidRoutes();

View File

@@ -0,0 +1,251 @@
import { Application, Request, Response } from 'express';
import config from "../../config";
import logger from '../../logger';
import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
import BlocksRepository from '../../repositories/BlocksRepository';
import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjustmentsRepository';
import HashratesRepository from '../../repositories/HashratesRepository';
import bitcoinClient from '../bitcoin/bitcoin-client';
import mining from "./mining";
class MiningRoutes {
public initRoutes(app: Application) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/:interval', this.$getPools)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/hashrate', this.$getPoolHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks', this.$getPoolBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks/:height', this.$getPoolBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug', this.$getPool)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools/:interval', this.$getPoolsHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', this.$getHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments', this.$getDifficultyAdjustments)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', this.$getRewardStats)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', this.$getHistoricalBlockFees)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/rewards/:interval', this.$getHistoricalBlockRewards)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', this.$getHistoricalBlockFeeRates)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', this.$getDifficultyAdjustments)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', this.$getHistoricalBlockPrediction)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/:hash', this.$getBlockAudit)
;
}
private async $getPool(req: Request, res: Response): Promise<void> {
try {
const stats = await mining.$getPoolStat(req.params.slug);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(stats);
} catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
res.status(404).send(e.message);
} else {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
private async $getPoolBlocks(req: Request, res: Response) {
try {
const poolBlocks = await BlocksRepository.$getBlocksByPool(
req.params.slug,
req.params.height === undefined ? undefined : parseInt(req.params.height, 10),
);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(poolBlocks);
} catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
res.status(404).send(e.message);
} else {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
private async $getPools(req: Request, res: Response) {
try {
const stats = await mining.$getPoolsStats(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(stats);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getPoolsHistoricalHashrate(req: Request, res: Response) {
try {
const hashrates = await HashratesRepository.$getPoolsWeeklyHashrate(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(hashrates);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getPoolHistoricalHashrate(req: Request, res: Response) {
try {
const hashrates = await HashratesRepository.$getPoolWeeklyHashrate(req.params.slug);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(hashrates);
} catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
res.status(404).send(e.message);
} else {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
private async $getHistoricalHashrate(req: Request, res: Response) {
let currentHashrate = 0, currentDifficulty = 0;
try {
currentHashrate = await bitcoinClient.getNetworkHashPs();
currentDifficulty = await bitcoinClient.getDifficulty();
} catch (e) {
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty');
}
try {
const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval);
const difficulty = await DifficultyAdjustmentsRepository.$getAdjustments(req.params.interval, false);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json({
hashrates: hashrates,
difficulty: difficulty,
currentHashrate: currentHashrate,
currentDifficulty: currentDifficulty,
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getHistoricalBlockFees(req: Request, res: Response) {
try {
const blockFees = await mining.$getHistoricalBlockFees(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getHistoricalBlockRewards(req: Request, res: Response) {
try {
const blockRewards = await mining.$getHistoricalBlockRewards(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockRewards);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getHistoricalBlockFeeRates(req: Request, res: Response) {
try {
const blockFeeRates = await mining.$getHistoricalBlockFeeRates(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFeeRates);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getHistoricalBlockSizeAndWeight(req: Request, res: Response) {
try {
const blockSizes = await mining.$getHistoricalBlockSizes(req.params.interval);
const blockWeights = await mining.$getHistoricalBlockWeights(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json({
sizes: blockSizes,
weights: blockWeights
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getDifficultyAdjustments(req: Request, res: Response) {
try {
const difficulty = await DifficultyAdjustmentsRepository.$getRawAdjustments(req.params.interval, true);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getRewardStats(req: Request, res: Response) {
try {
const response = await mining.$getRewardStats(parseInt(req.params.blockCount, 10));
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(response);
} catch (e) {
res.status(500).end();
}
}
private async $getHistoricalBlockPrediction(req: Request, res: Response) {
try {
const blockPredictions = await mining.$getBlockPredictionsHistory(req.params.interval);
const blockCount = await BlocksAuditsRepository.$getPredictionsCount();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockPredictions.map(prediction => [prediction.time, prediction.height, prediction.match_rate]));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async $getBlockAudit(req: Request, res: Response) {
try {
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
export default new MiningRoutes();

View File

@@ -1,23 +1,39 @@
import { PoolInfo, PoolStats, RewardStats } from '../mempool.interfaces';
import BlocksRepository from '../repositories/BlocksRepository';
import PoolsRepository from '../repositories/PoolsRepository';
import HashratesRepository from '../repositories/HashratesRepository';
import bitcoinClient from './bitcoin/bitcoin-client';
import logger from '../logger';
import { Common } from './common';
import loadingIndicators from './loading-indicators';
import { BlockPrice, PoolInfo, PoolStats, RewardStats } from '../../mempool.interfaces';
import BlocksRepository from '../../repositories/BlocksRepository';
import PoolsRepository from '../../repositories/PoolsRepository';
import HashratesRepository from '../../repositories/HashratesRepository';
import bitcoinClient from '../bitcoin/bitcoin-client';
import logger from '../../logger';
import { Common } from '../common';
import loadingIndicators from '../loading-indicators';
import { escape } from 'mysql2';
import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjustmentsRepository';
import config from '../../config';
import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
import PricesRepository from '../../repositories/PricesRepository';
class Mining {
blocksPriceIndexingRunning = false;
constructor() {
}
/**
* Get historical block predictions match rate
*/
public async $getBlockPredictionsHistory(interval: string | null = null): Promise<any> {
return await BlocksAuditsRepository.$getBlockPredictionsHistory(
this.getTimeRange(interval),
Common.getSqlInterval(interval)
);
}
/**
* Get historical block total fee
*/
public async $getHistoricalBlockFees(interval: string | null = null): Promise<any> {
return await BlocksRepository.$getHistoricalBlockFees(
this.getTimeRange(interval),
this.getTimeRange(interval, 5),
Common.getSqlInterval(interval)
);
}
@@ -159,26 +175,25 @@ class Mining {
*/
public async $generatePoolHashrateHistory(): Promise<void> {
const now = new Date();
const lastestRunDate = await HashratesRepository.$getLatestRun('last_weekly_hashrates_indexing');
try {
const lastestRunDate = await HashratesRepository.$getLatestRun('last_weekly_hashrates_indexing');
// Run only if:
// * lastestRunDate is set to 0 (node backend restart, reorg)
// * we started a new week (around Monday midnight)
const runIndexing = lastestRunDate === 0 || now.getUTCDay() === 1 && lastestRunDate !== now.getUTCDate();
if (!runIndexing) {
return;
}
} catch (e) {
throw e;
// Run only if:
// * lastestRunDate is set to 0 (node backend restart, reorg)
// * we started a new week (around Monday midnight)
const runIndexing = lastestRunDate === 0 || now.getUTCDay() === 1 && lastestRunDate !== now.getUTCDate();
if (!runIndexing) {
return;
}
try {
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
const genesisBlock = await bitcoinClient.getBlock(await bitcoinClient.getBlockHash(0));
const genesisTimestamp = genesisBlock.time * 1000;
const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps();
const hashrates: any[] = [];
const genesisTimestamp = 1231006505000; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
const lastMonday = new Date(now.setDate(now.getDate() - (now.getDay() + 6) % 7));
const lastMondayMidnight = this.getDateMidnight(lastMonday);
let toTimestamp = lastMondayMidnight.getTime();
@@ -193,7 +208,7 @@ class Mining {
logger.debug(`Indexing weekly mining pool hashrate`);
loadingIndicators.setProgress('weekly-hashrate-indexing', 0);
while (toTimestamp > genesisTimestamp) {
while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
const fromTimestamp = toTimestamp - 604800000;
// Skip already indexed weeks
@@ -203,14 +218,6 @@ class Mining {
continue;
}
// Check if we have blocks for the previous week (which mean that the week
// we are currently indexing has complete data)
const blockStatsPreviousWeek: any = await BlocksRepository.$blockCountBetweenTimestamp(
null, (fromTimestamp - 604800000) / 1000, (toTimestamp - 604800000) / 1000);
if (blockStatsPreviousWeek.blockCount === 0) { // We are done indexing
break;
}
const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
null, fromTimestamp / 1000, toTimestamp / 1000);
const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
@@ -218,34 +225,35 @@ class Mining {
let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp / 1000, toTimestamp / 1000);
const totalBlocks = pools.reduce((acc, pool) => acc + pool.blockCount, 0);
pools = pools.map((pool: any) => {
pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate;
pool.share = (pool.blockCount / totalBlocks);
return pool;
});
for (const pool of pools) {
hashrates.push({
hashrateTimestamp: toTimestamp / 1000,
avgHashrate: pool['hashrate'],
poolId: pool.poolId,
share: pool['share'],
type: 'weekly',
if (totalBlocks > 0) {
pools = pools.map((pool: any) => {
pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate;
pool.share = (pool.blockCount / totalBlocks);
return pool;
});
}
newlyIndexed += hashrates.length;
await HashratesRepository.$saveHashrates(hashrates);
hashrates.length = 0;
for (const pool of pools) {
hashrates.push({
hashrateTimestamp: toTimestamp / 1000,
avgHashrate: pool['hashrate'] ,
poolId: pool.poolId,
share: pool['share'],
type: 'weekly',
});
}
newlyIndexed += hashrates.length;
await HashratesRepository.$saveHashrates(hashrates);
hashrates.length = 0;
}
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
if (elapsedSeconds > 1) {
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const weeksPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totalIndexed / totalWeekIndexed * 10000) / 100;
const timeLeft = Math.round((totalWeekIndexed - totalIndexed) / weeksPerSeconds);
const formattedDate = new Date(fromTimestamp).toUTCString();
logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds.toFixed(2)} weeks/sec | total: ~${totalIndexed}/${Math.round(totalWeekIndexed)} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`);
logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds.toFixed(2)} weeks/sec | total: ~${totalIndexed}/${Math.round(totalWeekIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`);
timer = new Date().getTime() / 1000;
indexedThisRun = 0;
loadingIndicators.setProgress('weekly-hashrate-indexing', progress, false);
@@ -257,11 +265,14 @@ class Mining {
}
await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', new Date().getUTCDate());
if (newlyIndexed > 0) {
logger.info(`Indexed ${newlyIndexed} pools weekly hashrate`);
logger.notice(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`);
} else {
logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`);
}
loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
} catch (e) {
loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
logger.err(`Weekly mining pools hashrates indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
throw e;
}
}
@@ -270,20 +281,19 @@ class Mining {
* [INDEXING] Generate daily hashrate data
*/
public async $generateNetworkHashrateHistory(): Promise<void> {
try {
// We only run this once a day around midnight
const latestRunDate = await HashratesRepository.$getLatestRun('last_hashrates_indexing');
const now = new Date().getUTCDate();
if (now === latestRunDate) {
return;
}
} catch (e) {
throw e;
// We only run this once a day around midnight
const latestRunDate = await HashratesRepository.$getLatestRun('last_hashrates_indexing');
const now = new Date().getUTCDate();
if (now === latestRunDate) {
return;
}
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
try {
const indexedTimestamp = (await HashratesRepository.$getNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
const genesisTimestamp = 1231006505000; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
const genesisBlock = await bitcoinClient.getBlock(await bitcoinClient.getBlockHash(0));
const genesisTimestamp = genesisBlock.time * 1000;
const indexedTimestamp = (await HashratesRepository.$getRawNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
const lastMidnight = this.getDateMidnight(new Date());
let toTimestamp = Math.round(lastMidnight.getTime());
const hashrates: any[] = [];
@@ -298,27 +308,19 @@ class Mining {
logger.debug(`Indexing daily network hashrate`);
loadingIndicators.setProgress('daily-hashrate-indexing', 0);
while (toTimestamp > genesisTimestamp) {
while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
const fromTimestamp = toTimestamp - 86400000;
// Skip already indexed weeks
// Skip already indexed days
if (indexedTimestamp.includes(toTimestamp / 1000)) {
toTimestamp -= 86400000;
++totalIndexed;
continue;
}
// Check if we have blocks for the previous day (which mean that the day
// we are currently indexing has complete data)
const blockStatsPreviousDay: any = await BlocksRepository.$blockCountBetweenTimestamp(
null, (fromTimestamp - 86400000) / 1000, (toTimestamp - 86400000) / 1000);
if (blockStatsPreviousDay.blockCount === 0) { // We are done indexing
break;
}
const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
null, fromTimestamp / 1000, toTimestamp / 1000);
const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
const lastBlockHashrate = blockStats.blockCount === 0 ? 0 : await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
blockStats.lastBlockHeight);
hashrates.push({
@@ -340,9 +342,8 @@ class Mining {
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totalIndexed / totalDayIndexed * 10000) / 100;
const timeLeft = Math.round((totalDayIndexed - totalIndexed) / daysPerSeconds);
const formattedDate = new Date(fromTimestamp).toUTCString();
logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds.toFixed(2)} days/sec | total: ~${totalIndexed}/${Math.round(totalDayIndexed)} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`);
logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds.toFixed(2)} days/sec | total: ~${totalIndexed}/${Math.round(totalDayIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`);
timer = new Date().getTime() / 1000;
indexedThisRun = 0;
loadingIndicators.setProgress('daily-hashrate-indexing', progress);
@@ -354,11 +355,12 @@ class Mining {
}
// Add genesis block manually
if (toTimestamp <= genesisTimestamp && !indexedTimestamp.includes(genesisTimestamp)) {
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === -1 && !indexedTimestamp.includes(genesisTimestamp / 1000)) {
hashrates.push({
hashrateTimestamp: genesisTimestamp,
hashrateTimestamp: genesisTimestamp / 1000,
avgHashrate: await bitcoinClient.getNetworkHashPs(1, 1),
poolId: null,
poolId: 0,
share: 1,
type: 'daily',
});
}
@@ -368,15 +370,155 @@ class Mining {
await HashratesRepository.$setLatestRun('last_hashrates_indexing', new Date().getUTCDate());
if (newlyIndexed > 0) {
logger.info(`Indexed ${newlyIndexed} day of network hashrate`);
logger.notice(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`);
} else {
logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`);
}
loadingIndicators.setProgress('daily-hashrate-indexing', 100);
} catch (e) {
loadingIndicators.setProgress('daily-hashrate-indexing', 100);
logger.err(`Daily network hashrate indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
throw e;
}
}
/**
* Index difficulty adjustments
*/
public async $indexDifficultyAdjustments(): Promise<void> {
const indexedHeightsArray = await DifficultyAdjustmentsRepository.$getAdjustmentsHeights();
const indexedHeights = {};
for (const height of indexedHeightsArray) {
indexedHeights[height] = true;
}
const blocks: any = await BlocksRepository.$getBlocksDifficulty();
const genesisBlock = await bitcoinClient.getBlock(await bitcoinClient.getBlockHash(0));
let currentDifficulty = genesisBlock.difficulty;
let totalIndexed = 0;
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === -1 && indexedHeights[0] !== true) {
await DifficultyAdjustmentsRepository.$saveAdjustments({
time: genesisBlock.time,
height: 0,
difficulty: currentDifficulty,
adjustment: 0.0,
});
}
const oldestConsecutiveBlock = await BlocksRepository.$getOldestConsecutiveBlock();
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== -1) {
currentDifficulty = oldestConsecutiveBlock.difficulty;
}
let totalBlockChecked = 0;
let timer = new Date().getTime() / 1000;
for (const block of blocks) {
if (block.difficulty !== currentDifficulty) {
if (indexedHeights[block.height] === true) { // Already indexed
if (block.height >= oldestConsecutiveBlock.height) {
currentDifficulty = block.difficulty;
}
continue;
}
let adjustment = block.difficulty / currentDifficulty;
adjustment = Math.round(adjustment * 1000000) / 1000000; // Remove float point noise
await DifficultyAdjustmentsRepository.$saveAdjustments({
time: block.time,
height: block.height,
difficulty: block.difficulty,
adjustment: adjustment,
});
totalIndexed++;
if (block.height >= oldestConsecutiveBlock.height) {
currentDifficulty = block.difficulty;
}
}
totalBlockChecked++;
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
if (elapsedSeconds > 5) {
const progress = Math.round(totalBlockChecked / blocks.length * 100);
logger.info(`Indexing difficulty adjustment at block #${block.height} | Progress: ${progress}%`);
timer = new Date().getTime() / 1000;
}
}
if (totalIndexed > 0) {
logger.notice(`Indexed ${totalIndexed} difficulty adjustments`);
} else {
logger.debug(`Indexed ${totalIndexed} difficulty adjustments`);
}
}
/**
* Create a link between blocks and the latest price at when they were mined
*/
public async $indexBlockPrices() {
if (this.blocksPriceIndexingRunning === true) {
return;
}
this.blocksPriceIndexingRunning = true;
try {
const prices: any[] = await PricesRepository.$getPricesTimesAndId();
const blocksWithoutPrices: any[] = await BlocksRepository.$getBlocksWithoutPrice();
let totalInserted = 0;
const blocksPrices: BlockPrice[] = [];
for (const block of blocksWithoutPrices) {
// Quick optimisation, out mtgox feed only goes back to 2010-07-19 02:00:00, so skip the first 68951 blocks
if (['mainnet', 'testnet'].includes(config.MEMPOOL.NETWORK) && block.height < 68951) {
blocksPrices.push({
height: block.height,
priceId: prices[0].id,
});
continue;
}
for (const price of prices) {
if (block.timestamp < price.time) {
blocksPrices.push({
height: block.height,
priceId: price.id,
});
break;
};
}
if (blocksPrices.length >= 100000) {
totalInserted += blocksPrices.length;
let logStr = `Linking ${blocksPrices.length} blocks to their closest price`;
if (blocksWithoutPrices.length > 200000) {
logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`;
}
logger.debug(logStr);
await BlocksRepository.$saveBlockPrices(blocksPrices);
blocksPrices.length = 0;
}
}
if (blocksPrices.length > 0) {
totalInserted += blocksPrices.length;
let logStr = `Linking ${blocksPrices.length} blocks to their closest price`;
if (blocksWithoutPrices.length > 200000) {
logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`;
}
logger.debug(logStr);
await BlocksRepository.$saveBlockPrices(blocksPrices);
}
} catch (e) {
this.blocksPriceIndexingRunning = false;
throw e;
}
this.blocksPriceIndexingRunning = false;
}
private getDateMidnight(date: Date): Date {
date.setUTCHours(0);
date.setUTCMinutes(0);
@@ -386,18 +528,18 @@ class Mining {
return date;
}
private getTimeRange(interval: string | null): number {
private getTimeRange(interval: string | null, scale = 1): number {
switch (interval) {
case '3y': return 43200; // 12h
case '2y': return 28800; // 8h
case '1y': return 28800; // 8h
case '6m': return 10800; // 3h
case '3m': return 7200; // 2h
case '1m': return 1800; // 30min
case '1w': return 300; // 5min
case '3d': return 1;
case '24h': return 1;
default: return 86400; // 24h
case '3y': return 43200 * scale; // 12h
case '2y': return 28800 * scale; // 8h
case '1y': return 28800 * scale; // 8h
case '6m': return 10800 * scale; // 3h
case '3m': return 7200 * scale; // 2h
case '1m': return 1800 * scale; // 30min
case '1w': return 300 * scale; // 5min
case '3d': return 1 * scale;
case '24h': return 1 * scale;
default: return 86400 * scale;
}
}
}

View File

@@ -1,6 +1,7 @@
import DB from '../database';
import logger from '../logger';
import config from '../config';
import BlocksRepository from '../repositories/BlocksRepository';
interface Pool {
name: string;
@@ -32,7 +33,6 @@ class PoolsParser {
// First we save every entries without paying attention to pool duplication
const poolsDuplicated: Pool[] = [];
logger.debug('Parse coinbase_tags');
const coinbaseTags = Object.entries(poolsJson['coinbase_tags']);
for (let i = 0; i < coinbaseTags.length; ++i) {
poolsDuplicated.push({
@@ -43,7 +43,6 @@ class PoolsParser {
'slug': ''
});
}
logger.debug('Parse payout_addresses');
const addressesTags = Object.entries(poolsJson['payout_addresses']);
for (let i = 0; i < addressesTags.length; ++i) {
poolsDuplicated.push({
@@ -56,7 +55,6 @@ class PoolsParser {
}
// Then, we find unique mining pool names
logger.debug('Identify unique mining pools');
const poolNames: string[] = [];
for (let i = 0; i < poolsDuplicated.length; ++i) {
if (poolNames.indexOf(poolsDuplicated[i].name) === -1) {
@@ -119,8 +117,15 @@ class PoolsParser {
'slug': slug
};
if (existingPools.find((pool) => pool.name === poolNames[i]) !== undefined) {
finalPoolDataUpdate.push(poolObj);
const existingPool = existingPools.find((pool) => pool.name === poolNames[i]);
if (existingPool !== undefined) {
// Check if any data was actually updated
const equals = (a, b) =>
a.length === b.length &&
a.every((v, i) => v === b[i]);
if (!equals(JSON.parse(existingPool.addresses), poolObj.addresses) || !equals(JSON.parse(existingPool.regexes), poolObj.regexes)) {
finalPoolDataUpdate.push(poolObj);
}
} else {
logger.debug(`Add '${finalPoolName}' mining pool`);
finalPoolDataAdd.push(poolObj);
@@ -140,40 +145,51 @@ class PoolsParser {
return;
}
logger.debug(`Update pools table now`);
if (finalPoolDataAdd.length > 0 || finalPoolDataUpdate.length > 0) {
logger.debug(`Update pools table now`);
// Add new mining pools into the database
let queryAdd: string = 'INSERT INTO pools(name, link, regexes, addresses, slug) VALUES ';
for (let i = 0; i < finalPoolDataAdd.length; ++i) {
queryAdd += `('${finalPoolDataAdd[i].name}', '${finalPoolDataAdd[i].link}',
'${JSON.stringify(finalPoolDataAdd[i].regexes)}', '${JSON.stringify(finalPoolDataAdd[i].addresses)}',
${JSON.stringify(finalPoolDataAdd[i].slug)}),`;
}
queryAdd = queryAdd.slice(0, -1) + ';';
// Add new mining pools into the database
let queryAdd: string = 'INSERT INTO pools(name, link, regexes, addresses, slug) VALUES ';
for (let i = 0; i < finalPoolDataAdd.length; ++i) {
queryAdd += `('${finalPoolDataAdd[i].name}', '${finalPoolDataAdd[i].link}',
'${JSON.stringify(finalPoolDataAdd[i].regexes)}', '${JSON.stringify(finalPoolDataAdd[i].addresses)}',
${JSON.stringify(finalPoolDataAdd[i].slug)}),`;
}
queryAdd = queryAdd.slice(0, -1) + ';';
// Updated existing mining pools in the database
const updateQueries: string[] = [];
for (let i = 0; i < finalPoolDataUpdate.length; ++i) {
updateQueries.push(`
UPDATE pools
SET name='${finalPoolDataUpdate[i].name}', link='${finalPoolDataUpdate[i].link}',
regexes='${JSON.stringify(finalPoolDataUpdate[i].regexes)}', addresses='${JSON.stringify(finalPoolDataUpdate[i].addresses)}',
slug='${finalPoolDataUpdate[i].slug}'
WHERE name='${finalPoolDataUpdate[i].name}'
;`);
// Updated existing mining pools in the database
const updateQueries: string[] = [];
for (let i = 0; i < finalPoolDataUpdate.length; ++i) {
updateQueries.push(`
UPDATE pools
SET name='${finalPoolDataUpdate[i].name}', link='${finalPoolDataUpdate[i].link}',
regexes='${JSON.stringify(finalPoolDataUpdate[i].regexes)}', addresses='${JSON.stringify(finalPoolDataUpdate[i].addresses)}',
slug='${finalPoolDataUpdate[i].slug}'
WHERE name='${finalPoolDataUpdate[i].name}'
;`);
}
try {
await this.$deleteBlocskToReindex(finalPoolDataUpdate);
if (finalPoolDataAdd.length > 0) {
await DB.query({ sql: queryAdd, timeout: 120000 });
}
for (const query of updateQueries) {
await DB.query({ sql: query, timeout: 120000 });
}
await this.insertUnknownPool();
logger.info('Mining pools.json import completed');
} catch (e) {
logger.err(`Cannot import pools in the database`);
throw e;
}
}
try {
if (finalPoolDataAdd.length > 0) {
await DB.query({ sql: queryAdd, timeout: 120000 });
}
for (const query of updateQueries) {
await DB.query({ sql: query, timeout: 120000 });
}
await this.insertUnknownPool();
logger.info('Mining pools.json import completed');
} catch (e) {
logger.err(`Cannot import pools in the database`);
logger.err(`Cannot insert unknown pool in the database`);
throw e;
}
}
@@ -201,6 +217,36 @@ class PoolsParser {
logger.err('Unable to insert "Unknown" mining pool');
}
}
/**
* Delete blocks which needs to be reindexed
*/
private async $deleteBlocskToReindex(finalPoolDataUpdate: any[]) {
if (config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING === false) {
return;
}
const blockCount = await BlocksRepository.$blockCount(null, null);
if (blockCount === 0) {
return;
}
for (const updatedPool of finalPoolDataUpdate) {
const [pool]: any[] = await DB.query(`SELECT id, name from pools where slug = "${updatedPool.slug}"`);
if (pool.length > 0) {
logger.notice(`Deleting blocks from ${pool[0].name} mining pool for future re-indexing`);
await DB.query(`DELETE FROM blocks WHERE pool_id = ${pool[0].id}`);
}
}
// Ignore early days of Bitcoin as there were not mining pool yet
logger.notice('Deleting blocks with unknown mining pool from height 130635 for future re-indexing');
const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`);
await DB.query(`DELETE FROM blocks WHERE pool_id = ${unknownPool[0].id} AND height > 130635`);
logger.notice('Truncating hashrates for future re-indexing');
await DB.query(`DELETE FROM hashrates`);
}
}
export default new PoolsParser();

View File

@@ -1,160 +1,11 @@
import memPool from './mempool';
import DB from '../database';
import logger from '../logger';
import DB from '../../database';
import logger from '../../logger';
import { Statistic, OptimizedStatistic } from '../../mempool.interfaces';
import { Statistic, TransactionExtended, OptimizedStatistic } from '../mempool.interfaces';
import config from '../config';
import { Common } from './common';
class Statistics {
protected intervalTimer: NodeJS.Timer | undefined;
protected newStatisticsEntryCallback: ((stats: OptimizedStatistic) => void) | undefined;
class StatisticsApi {
protected queryTimeout = 120000;
public setNewStatisticsEntryCallback(fn: (stats: OptimizedStatistic) => void) {
this.newStatisticsEntryCallback = fn;
}
constructor() { }
public startStatistics(): void {
logger.info('Starting statistics service');
const now = new Date();
const nextInterval = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(),
Math.floor(now.getMinutes() / 1) * 1 + 1, 0, 0);
const difference = nextInterval.getTime() - now.getTime();
setTimeout(() => {
this.runStatistics();
this.intervalTimer = setInterval(() => {
this.runStatistics();
}, 1 * 60 * 1000);
}, difference);
}
private async runStatistics(): Promise<void> {
if (!memPool.isInSync()) {
return;
}
const currentMempool = memPool.getMempool();
const txPerSecond = memPool.getTxPerSecond();
const vBytesPerSecond = memPool.getVBytesPerSecond();
logger.debug('Running statistics');
let memPoolArray: TransactionExtended[] = [];
for (const i in currentMempool) {
if (currentMempool.hasOwnProperty(i)) {
memPoolArray.push(currentMempool[i]);
}
}
// Remove 0 and undefined
memPoolArray = memPoolArray.filter((tx) => tx.effectiveFeePerVsize);
if (!memPoolArray.length) {
try {
const insertIdZeroed = await this.$createZeroedStatistic();
if (this.newStatisticsEntryCallback && insertIdZeroed) {
const newStats = await this.$get(insertIdZeroed);
if (newStats) {
this.newStatisticsEntryCallback(newStats);
}
}
} catch (e) {
logger.err('Unable to insert zeroed statistics. ' + e);
}
return;
}
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);
const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
const weightVsizeFees: { [feePerWU: number]: number } = {};
const lastItem = logFees.length - 1;
memPoolArray.forEach((transaction) => {
for (let i = 0; i < logFees.length; i++) {
if (
(Common.isLiquid() && (i === lastItem || transaction.effectiveFeePerVsize * 10 < logFees[i + 1]))
||
(!Common.isLiquid() && (i === lastItem || transaction.effectiveFeePerVsize < logFees[i + 1]))
) {
if (weightVsizeFees[logFees[i]]) {
weightVsizeFees[logFees[i]] += transaction.vsize;
} else {
weightVsizeFees[logFees[i]] = transaction.vsize;
}
break;
}
}
});
try {
const insertId = await this.$create({
added: 'NOW()',
unconfirmed_transactions: memPoolArray.length,
tx_per_second: txPerSecond,
vbytes_per_second: Math.round(vBytesPerSecond),
mempool_byte_weight: totalWeight,
total_fee: totalFee,
fee_data: '',
vsize_1: weightVsizeFees['1'] || 0,
vsize_2: weightVsizeFees['2'] || 0,
vsize_3: weightVsizeFees['3'] || 0,
vsize_4: weightVsizeFees['4'] || 0,
vsize_5: weightVsizeFees['5'] || 0,
vsize_6: weightVsizeFees['6'] || 0,
vsize_8: weightVsizeFees['8'] || 0,
vsize_10: weightVsizeFees['10'] || 0,
vsize_12: weightVsizeFees['12'] || 0,
vsize_15: weightVsizeFees['15'] || 0,
vsize_20: weightVsizeFees['20'] || 0,
vsize_30: weightVsizeFees['30'] || 0,
vsize_40: weightVsizeFees['40'] || 0,
vsize_50: weightVsizeFees['50'] || 0,
vsize_60: weightVsizeFees['60'] || 0,
vsize_70: weightVsizeFees['70'] || 0,
vsize_80: weightVsizeFees['80'] || 0,
vsize_90: weightVsizeFees['90'] || 0,
vsize_100: weightVsizeFees['100'] || 0,
vsize_125: weightVsizeFees['125'] || 0,
vsize_150: weightVsizeFees['150'] || 0,
vsize_175: weightVsizeFees['175'] || 0,
vsize_200: weightVsizeFees['200'] || 0,
vsize_250: weightVsizeFees['250'] || 0,
vsize_300: weightVsizeFees['300'] || 0,
vsize_350: weightVsizeFees['350'] || 0,
vsize_400: weightVsizeFees['400'] || 0,
vsize_500: weightVsizeFees['500'] || 0,
vsize_600: weightVsizeFees['600'] || 0,
vsize_700: weightVsizeFees['700'] || 0,
vsize_800: weightVsizeFees['800'] || 0,
vsize_900: weightVsizeFees['900'] || 0,
vsize_1000: weightVsizeFees['1000'] || 0,
vsize_1200: weightVsizeFees['1200'] || 0,
vsize_1400: weightVsizeFees['1400'] || 0,
vsize_1600: weightVsizeFees['1600'] || 0,
vsize_1800: weightVsizeFees['1800'] || 0,
vsize_2000: weightVsizeFees['2000'] || 0,
});
if (this.newStatisticsEntryCallback && insertId) {
const newStats = await this.$get(insertId);
if (newStats) {
this.newStatisticsEntryCallback(newStats);
}
}
} catch (e) {
logger.err('Unable to insert statistics. ' + e);
}
}
private async $createZeroedStatistic(): Promise<number | undefined> {
public async $createZeroedStatistic(): Promise<number | undefined> {
try {
const query = `INSERT INTO statistics(
added,
@@ -212,7 +63,7 @@ class Statistics {
}
}
private async $create(statistics: Statistic): Promise<number | undefined> {
public async $create(statistics: Statistic): Promise<number | undefined> {
try {
const query = `INSERT INTO statistics(
added,
@@ -413,7 +264,7 @@ class Statistics {
ORDER BY statistics.added DESC;`;
}
private async $get(id: number): Promise<OptimizedStatistic | undefined> {
public async $get(id: number): Promise<OptimizedStatistic | undefined> {
try {
const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics WHERE id = ?`;
const [rows] = await DB.query(query, [id]);
@@ -574,7 +425,6 @@ class Statistics {
};
});
}
}
export default new Statistics();
export default new StatisticsApi();

View File

@@ -0,0 +1,67 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import statisticsApi from './statistics-api';
class StatisticsRoutes {
public initRoutes(app: Application) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2h', this.$getStatisticsByTime.bind(this, '2h'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/24h', this.$getStatisticsByTime.bind(this, '24h'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1w', this.$getStatisticsByTime.bind(this, '1w'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1m', this.$getStatisticsByTime.bind(this, '1m'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3m', this.$getStatisticsByTime.bind(this, '3m'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/6m', this.$getStatisticsByTime.bind(this, '6m'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', this.$getStatisticsByTime.bind(this, '1y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', this.$getStatisticsByTime.bind(this, '2y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', this.$getStatisticsByTime.bind(this, '3y'))
;
}
private async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y', req: Request, res: Response) {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
try {
let result;
switch (time as string) {
case '2h':
result = await statisticsApi.$list2H();
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
break;
case '24h':
result = await statisticsApi.$list24H();
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
break;
case '1w':
result = await statisticsApi.$list1W();
break;
case '1m':
result = await statisticsApi.$list1M();
break;
case '3m':
result = await statisticsApi.$list3M();
break;
case '6m':
result = await statisticsApi.$list6M();
break;
case '1y':
result = await statisticsApi.$list1Y();
break;
case '2y':
result = await statisticsApi.$list2Y();
break;
case '3y':
result = await statisticsApi.$list3Y();
break;
default:
result = await statisticsApi.$list2H();
}
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
export default new StatisticsRoutes();

View File

@@ -0,0 +1,153 @@
import memPool from '../mempool';
import logger from '../../logger';
import { TransactionExtended, OptimizedStatistic } from '../../mempool.interfaces';
import { Common } from '../common';
import statisticsApi from './statistics-api';
class Statistics {
protected intervalTimer: NodeJS.Timer | undefined;
protected newStatisticsEntryCallback: ((stats: OptimizedStatistic) => void) | undefined;
public setNewStatisticsEntryCallback(fn: (stats: OptimizedStatistic) => void) {
this.newStatisticsEntryCallback = fn;
}
public startStatistics(): void {
logger.info('Starting statistics service');
const now = new Date();
const nextInterval = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(),
Math.floor(now.getMinutes() / 1) * 1 + 1, 0, 0);
const difference = nextInterval.getTime() - now.getTime();
setTimeout(() => {
this.runStatistics();
this.intervalTimer = setInterval(() => {
this.runStatistics();
}, 1 * 60 * 1000);
}, difference);
}
private async runStatistics(): Promise<void> {
if (!memPool.isInSync()) {
return;
}
const currentMempool = memPool.getMempool();
const txPerSecond = memPool.getTxPerSecond();
const vBytesPerSecond = memPool.getVBytesPerSecond();
logger.debug('Running statistics');
let memPoolArray: TransactionExtended[] = [];
for (const i in currentMempool) {
if (currentMempool.hasOwnProperty(i)) {
memPoolArray.push(currentMempool[i]);
}
}
// Remove 0 and undefined
memPoolArray = memPoolArray.filter((tx) => tx.effectiveFeePerVsize);
if (!memPoolArray.length) {
try {
const insertIdZeroed = await statisticsApi.$createZeroedStatistic();
if (this.newStatisticsEntryCallback && insertIdZeroed) {
const newStats = await statisticsApi.$get(insertIdZeroed);
if (newStats) {
this.newStatisticsEntryCallback(newStats);
}
}
} catch (e) {
logger.err('Unable to insert zeroed statistics. ' + e);
}
return;
}
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);
const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
const weightVsizeFees: { [feePerWU: number]: number } = {};
const lastItem = logFees.length - 1;
memPoolArray.forEach((transaction) => {
for (let i = 0; i < logFees.length; i++) {
if (
(Common.isLiquid() && (i === lastItem || transaction.effectiveFeePerVsize * 10 < logFees[i + 1]))
||
(!Common.isLiquid() && (i === lastItem || transaction.effectiveFeePerVsize < logFees[i + 1]))
) {
if (weightVsizeFees[logFees[i]]) {
weightVsizeFees[logFees[i]] += transaction.vsize;
} else {
weightVsizeFees[logFees[i]] = transaction.vsize;
}
break;
}
}
});
try {
const insertId = await statisticsApi.$create({
added: 'NOW()',
unconfirmed_transactions: memPoolArray.length,
tx_per_second: txPerSecond,
vbytes_per_second: Math.round(vBytesPerSecond),
mempool_byte_weight: totalWeight,
total_fee: totalFee,
fee_data: '',
vsize_1: weightVsizeFees['1'] || 0,
vsize_2: weightVsizeFees['2'] || 0,
vsize_3: weightVsizeFees['3'] || 0,
vsize_4: weightVsizeFees['4'] || 0,
vsize_5: weightVsizeFees['5'] || 0,
vsize_6: weightVsizeFees['6'] || 0,
vsize_8: weightVsizeFees['8'] || 0,
vsize_10: weightVsizeFees['10'] || 0,
vsize_12: weightVsizeFees['12'] || 0,
vsize_15: weightVsizeFees['15'] || 0,
vsize_20: weightVsizeFees['20'] || 0,
vsize_30: weightVsizeFees['30'] || 0,
vsize_40: weightVsizeFees['40'] || 0,
vsize_50: weightVsizeFees['50'] || 0,
vsize_60: weightVsizeFees['60'] || 0,
vsize_70: weightVsizeFees['70'] || 0,
vsize_80: weightVsizeFees['80'] || 0,
vsize_90: weightVsizeFees['90'] || 0,
vsize_100: weightVsizeFees['100'] || 0,
vsize_125: weightVsizeFees['125'] || 0,
vsize_150: weightVsizeFees['150'] || 0,
vsize_175: weightVsizeFees['175'] || 0,
vsize_200: weightVsizeFees['200'] || 0,
vsize_250: weightVsizeFees['250'] || 0,
vsize_300: weightVsizeFees['300'] || 0,
vsize_350: weightVsizeFees['350'] || 0,
vsize_400: weightVsizeFees['400'] || 0,
vsize_500: weightVsizeFees['500'] || 0,
vsize_600: weightVsizeFees['600'] || 0,
vsize_700: weightVsizeFees['700'] || 0,
vsize_800: weightVsizeFees['800'] || 0,
vsize_900: weightVsizeFees['900'] || 0,
vsize_1000: weightVsizeFees['1000'] || 0,
vsize_1200: weightVsizeFees['1200'] || 0,
vsize_1400: weightVsizeFees['1400'] || 0,
vsize_1600: weightVsizeFees['1600'] || 0,
vsize_1800: weightVsizeFees['1800'] || 0,
vsize_2000: weightVsizeFees['2000'] || 0,
});
if (this.newStatisticsEntryCallback && insertId) {
const newStats = await statisticsApi.$get(insertId);
if (newStats) {
this.newStatisticsEntryCallback(newStats);
}
}
} catch (e) {
logger.err('Unable to insert statistics. ' + e);
}
}
}
export default new Statistics();

View File

@@ -1,7 +1,9 @@
import logger from '../logger';
import * as WebSocket from 'ws';
import { BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock, MempoolBlockDelta,
OptimizedStatistic, ILoadingIndicators, IConversionRates } from '../mempool.interfaces';
import {
BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock, MempoolBlockDelta,
OptimizedStatistic, ILoadingIndicators, IConversionRates
} from '../mempool.interfaces';
import blocks from './blocks';
import memPool from './mempool';
import backendInfo from './backend-info';
@@ -14,6 +16,8 @@ import transactionUtils from './transaction-utils';
import rbfCache from './rbf-cache';
import difficultyAdjustment from './difficulty-adjustment';
import feeApi from './fee-api';
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
class WebsocketHandler {
private wss: WebSocket.Server | undefined;
@@ -164,7 +168,7 @@ class WebsocketHandler {
throw new Error('WebSocket.Server is not set');
}
this.wss.clients.forEach((client: WebSocket) => {
this.wss.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
@@ -179,7 +183,7 @@ class WebsocketHandler {
throw new Error('WebSocket.Server is not set');
}
this.wss.clients.forEach((client: WebSocket) => {
this.wss.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
@@ -192,7 +196,7 @@ class WebsocketHandler {
throw new Error('WebSocket.Server is not set');
}
this.wss.clients.forEach((client: WebSocket) => {
this.wss.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
@@ -224,7 +228,7 @@ class WebsocketHandler {
throw new Error('WebSocket.Server is not set');
}
this.wss.clients.forEach((client: WebSocket) => {
this.wss.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
@@ -255,7 +259,7 @@ class WebsocketHandler {
memPool.handleRbfTransactions(rbfTransactions);
const recommendedFees = feeApi.getRecommendedFee();
this.wss.clients.forEach(async (client: WebSocket) => {
this.wss.clients.forEach(async (client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
@@ -414,17 +418,56 @@ class WebsocketHandler {
if (_mempoolBlocks[0]) {
const matches: string[] = [];
const added: string[] = [];
const missing: string[] = [];
for (const txId of txIds) {
if (_mempoolBlocks[0].transactionIds.indexOf(txId) > -1) {
matches.push(txId);
} else {
added.push(txId);
}
delete _memPool[txId];
}
matchRate = Math.round((matches.length / (txIds.length - 1)) * 100);
for (const txId of _mempoolBlocks[0].transactionIds) {
if (matches.includes(txId) || added.includes(txId)) {
continue;
}
missing.push(txId);
}
matchRate = Math.round((Math.max(0, matches.length - missing.length - added.length) / txIds.length * 100) * 100) / 100;
mempoolBlocks.updateMempoolBlocks(_memPool);
mBlocks = mempoolBlocks.getMempoolBlocks();
mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
if (Common.indexingEnabled()) {
const stripped = _mempoolBlocks[0].transactions.map((tx) => {
return {
txid: tx.txid,
vsize: tx.vsize,
fee: tx.fee ? Math.round(tx.fee) : 0,
value: tx.value,
};
});
BlocksSummariesRepository.$saveSummary({
height: block.height,
template: {
id: block.id,
transactions: stripped
}
});
BlocksAuditsRepository.$saveAudit({
time: block.timestamp,
height: block.height,
hash: block.id,
addedTxs: added,
missingTxs: missing,
matchRate: matchRate,
});
}
}
if (block.extras) {

View File

@@ -1,4 +1,6 @@
const configFile = require('../mempool-config.json');
const configFromFile = require(
process.env.MEMPOOL_CONFIG_FILE ? process.env.MEMPOOL_CONFIG_FILE : '../mempool-config.json'
);
interface IConfig {
MEMPOOL: {
@@ -15,6 +17,7 @@ interface IConfig {
INITIAL_BLOCKS_AMOUNT: number;
MEMPOOL_BLOCKS_AMOUNT: number;
INDEXING_BLOCKS_AMOUNT: number;
BLOCKS_SUMMARIES_INDEXING: boolean;
PRICE_FEED_UPDATE_INTERVAL: number;
USE_SECOND_NODE_FOR_MINFEE: boolean;
EXTERNAL_ASSETS: string[];
@@ -22,10 +25,29 @@ interface IConfig {
EXTERNAL_RETRY_INTERVAL: number;
USER_AGENT: string;
STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
AUTOMATIC_BLOCK_REINDEXING: boolean;
POOLS_JSON_URL: string,
POOLS_JSON_TREE_URL: string,
};
ESPLORA: {
REST_API_URL: string;
};
LIGHTNING: {
ENABLED: boolean;
BACKEND: 'lnd' | 'cln' | 'ldk';
TOPOLOGY_FOLDER: string;
STATS_REFRESH_INTERVAL: number;
GRAPH_REFRESH_INTERVAL: number;
LOGGER_UPDATE_INTERVAL: number;
};
LND: {
TLS_CERT_PATH: string;
MACAROON_PATH: string;
REST_API_URL: string;
};
CLIGHTNING: {
SOCKET: string;
};
ELECTRUM: {
HOST: string;
PORT: number;
@@ -87,6 +109,12 @@ interface IConfig {
BISQ_URL: string;
BISQ_ONION: string;
};
MAXMIND: {
ENABLED: boolean;
GEOLITE2_CITY: string;
GEOLITE2_ASN: string;
GEOIP2_ISP: string;
},
}
const defaults: IConfig = {
@@ -104,6 +132,7 @@ const defaults: IConfig = {
'INITIAL_BLOCKS_AMOUNT': 8,
'MEMPOOL_BLOCKS_AMOUNT': 8,
'INDEXING_BLOCKS_AMOUNT': 11000, // 0 = disable indexing, -1 = index all blocks
'BLOCKS_SUMMARIES_INDEXING': false,
'PRICE_FEED_UPDATE_INTERVAL': 600,
'USE_SECOND_NODE_FOR_MINFEE': false,
'EXTERNAL_ASSETS': [],
@@ -111,6 +140,9 @@ const defaults: IConfig = {
'EXTERNAL_RETRY_INTERVAL': 0,
'USER_AGENT': 'mempool',
'STDOUT_LOG_MIN_PRIORITY': 'debug',
'AUTOMATIC_BLOCK_REINDEXING': false,
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
},
'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000',
@@ -156,6 +188,22 @@ const defaults: IConfig = {
'ENABLED': false,
'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db'
},
'LIGHTNING': {
'ENABLED': false,
'BACKEND': 'lnd',
'TOPOLOGY_FOLDER': '',
'STATS_REFRESH_INTERVAL': 600,
'GRAPH_REFRESH_INTERVAL': 600,
'LOGGER_UPDATE_INTERVAL': 30,
},
'LND': {
'TLS_CERT_PATH': '',
'MACAROON_PATH': '',
'REST_API_URL': 'https://localhost:8080',
},
'CLIGHTNING': {
'SOCKET': '',
},
'SOCKS5PROXY': {
'ENABLED': false,
'USE_ONION': true,
@@ -164,18 +212,24 @@ const defaults: IConfig = {
'USERNAME': '',
'PASSWORD': ''
},
"PRICE_DATA_SERVER": {
'PRICE_DATA_SERVER': {
'TOR_URL': 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices',
'CLEARNET_URL': 'https://price.bisq.wiz.biz/getAllMarketPrices'
},
"EXTERNAL_DATA_SERVER": {
'EXTERNAL_DATA_SERVER': {
'MEMPOOL_API': 'https://mempool.space/api/v1',
'MEMPOOL_ONION': 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
'LIQUID_API': 'https://liquid.network/api/v1',
'LIQUID_ONION': 'http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1',
'BISQ_URL': 'https://bisq.markets/api',
'BISQ_ONION': 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
}
},
"MAXMIND": {
'ENABLED': false,
"GEOLITE2_CITY": "/usr/local/share/GeoIP/GeoLite2-City.mmdb",
"GEOLITE2_ASN": "/usr/local/share/GeoIP/GeoLite2-ASN.mmdb",
"GEOIP2_ISP": "/usr/local/share/GeoIP/GeoIP2-ISP.mmdb"
},
};
class Config implements IConfig {
@@ -188,12 +242,16 @@ class Config implements IConfig {
SYSLOG: IConfig['SYSLOG'];
STATISTICS: IConfig['STATISTICS'];
BISQ: IConfig['BISQ'];
LIGHTNING: IConfig['LIGHTNING'];
LND: IConfig['LND'];
CLIGHTNING: IConfig['CLIGHTNING'];
SOCKS5PROXY: IConfig['SOCKS5PROXY'];
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
MAXMIND: IConfig['MAXMIND'];
constructor() {
const configs = this.merge(configFile, defaults);
const configs = this.merge(configFromFile, defaults);
this.MEMPOOL = configs.MEMPOOL;
this.ESPLORA = configs.ESPLORA;
this.ELECTRUM = configs.ELECTRUM;
@@ -203,9 +261,13 @@ class Config implements IConfig {
this.SYSLOG = configs.SYSLOG;
this.STATISTICS = configs.STATISTICS;
this.BISQ = configs.BISQ;
this.LIGHTNING = configs.LIGHTNING;
this.LND = configs.LND;
this.CLIGHTNING = configs.CLIGHTNING;
this.SOCKS5PROXY = configs.SOCKS5PROXY;
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
this.MAXMIND = configs.MAXMIND;
}
merge = (...objects: object[]): IConfig => {

View File

@@ -1,7 +1,7 @@
import config from './config';
import { createPool, Pool, PoolConnection } from 'mysql2/promise';
import logger from './logger';
import { PoolOptions } from 'mysql2/typings/mysql';
import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } from 'mysql2/typings/mysql';
class DB {
constructor() {
@@ -28,7 +28,9 @@ import { PoolOptions } from 'mysql2/typings/mysql';
}
}
public async query(query, params?) {
public async query<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
OkPacket[] | ResultSetHeader>(query, params?): Promise<[T, FieldPacket[]]>
{
this.checkDBFlag();
const pool = await this.getPool();
return pool.query(query, params);

View File

@@ -1,17 +1,14 @@
import { Express, Request, Response, NextFunction } from 'express';
import * as express from 'express';
import express from "express";
import { Application, Request, Response, NextFunction } from 'express';
import * as http from 'http';
import * as WebSocket from 'ws';
import * as cluster from 'cluster';
import axios from 'axios';
import cluster from 'cluster';
import DB from './database';
import config from './config';
import routes from './routes';
import blocks from './api/blocks';
import memPool from './api/mempool';
import diskCache from './api/disk-cache';
import statistics from './api/statistics';
import statistics from './api/statistics/statistics';
import websocketHandler from './api/websocket-handler';
import fiatConversion from './api/fiat-conversion';
import bisq from './api/bisq/bisq';
@@ -27,11 +24,22 @@ import icons from './api/liquid/icons';
import { Common } from './api/common';
import poolsUpdater from './tasks/pools-updater';
import indexer from './indexer';
import nodesRoutes from './api/explorer/nodes.routes';
import channelsRoutes from './api/explorer/channels.routes';
import generalLightningRoutes from './api/explorer/general.routes';
import lightningStatsUpdater from './tasks/lightning/stats-updater.service';
import networkSyncService from './tasks/lightning/network-sync.service';
import statisticsRoutes from './api/statistics/statistics.routes';
import miningRoutes from './api/mining/mining-routes';
import bisqRoutes from './api/bisq/bisq.routes';
import liquidRoutes from './api/liquid/liquid.routes';
import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
import fundingTxFetcher from "./tasks/lightning/sync-tasks/funding-tx-fetcher";
class Server {
private wss: WebSocket.Server | undefined;
private server: http.Server | undefined;
private app: Express;
private app: Application;
private currentBackendRetryInterval = 5;
constructor() {
@@ -42,7 +50,7 @@ class Server {
return;
}
if (cluster.isMaster) {
if (cluster.isPrimary) {
logger.notice(`Mempool Server (Master) is running on port ${config.MEMPOOL.HTTP_PORT} (${backendInfo.getShortCommitHash()})`);
const numCPUs = config.MEMPOOL.SPAWN_CLUSTER_PROCS;
@@ -76,7 +84,7 @@ class Server {
})
.use(express.urlencoded({ extended: true }))
.use(express.text())
;
;
this.server = http.createServer(this.app);
this.wss = new WebSocket.Server({ server: this.server });
@@ -104,7 +112,7 @@ class Server {
}
}
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isMaster) {
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) {
statistics.startStatistics();
}
@@ -128,6 +136,10 @@ class Server {
bisqMarkets.startBisqService();
}
if (config.LIGHTNING.ENABLED) {
this.$runLightningBackend();
}
this.server.listen(config.MEMPOOL.HTTP_PORT, () => {
if (worker) {
logger.info(`Mempool Server worker #${process.pid} started`);
@@ -171,6 +183,18 @@ class Server {
}
}
async $runLightningBackend() {
try {
await fundingTxFetcher.$init();
await networkSyncService.$startService();
await lightningStatsUpdater.$startService();
} catch(e) {
logger.err(`Nodejs lightning backend crashed. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`);
await Common.sleep$(1000 * 60);
this.$runLightningBackend();
};
}
setUpWebsocketHandling() {
if (this.wss) {
websocketHandler.setWebsocketServer(this.wss);
@@ -193,165 +217,23 @@ 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 + 'difficulty-adjustment', routes.getDifficultyChange)
.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 + 'validate-address/:address', routes.validateAddress)
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', routes.$postTransactionForm)
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/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(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/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(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/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(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
;
bitcoinRoutes.initRoutes(this.app);
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2h', routes.$getStatisticsByTime.bind(routes, '2h'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/24h', routes.$getStatisticsByTime.bind(routes, '24h'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1w', routes.$getStatisticsByTime.bind(routes, '1w'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1m', routes.$getStatisticsByTime.bind(routes, '1m'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3m', routes.$getStatisticsByTime.bind(routes, '3m'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/6m', routes.$getStatisticsByTime.bind(routes, '6m'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', routes.$getStatisticsByTime.bind(routes, '1y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', routes.$getStatisticsByTime.bind(routes, '2y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', routes.$getStatisticsByTime.bind(routes, '3y'))
;
statisticsRoutes.initRoutes(this.app);
}
if (Common.indexingEnabled()) {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/:interval', routes.$getPools)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/hashrate', routes.$getPoolHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks', routes.$getPoolBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks/:height', routes.$getPoolBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug', routes.$getPool)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools/:interval', routes.$getPoolsHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', routes.$getHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', routes.$getRewardStats)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', routes.$getHistoricalBlockFees)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/rewards/:interval', routes.$getHistoricalBlockRewards)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', routes.$getHistoricalBlockFeeRates)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', routes.$getHistoricalBlockSizeAndWeight)
;
miningRoutes.initRoutes(this.app);
}
if (config.BISQ.ENABLED) {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/stats', routes.getBisqStats)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/tx/:txId', routes.getBisqTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/block/:hash', routes.getBisqBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/blocks/tip/height', routes.getBisqTip)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/blocks/:index/:length', routes.getBisqBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/address/:address', routes.getBisqAddress)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/txs/:index/:length', routes.getBisqTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/currencies', routes.getBisqMarketCurrencies.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/depth', routes.getBisqMarketDepth.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/hloc', routes.getBisqMarketHloc.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/markets', routes.getBisqMarketMarkets.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/offers', routes.getBisqMarketOffers.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/ticker', routes.getBisqMarketTicker.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/trades', routes.getBisqMarketTrades.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/volumes', routes.getBisqMarketVolumes.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/volumes/7d', routes.getBisqMarketVolumes7d.bind(routes))
;
bisqRoutes.initRoutes(this.app);
}
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', routes.getBlock);
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)
.post(config.MEMPOOL.API_URL_PREFIX + 'tx', routes.$postTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', routes.getRawTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', routes.getTransactionStatus)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', routes.getTransactionOutspends)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', routes.getBlockHeader)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', routes.getBlockTipHeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs', routes.getBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs/:index', routes.getBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', routes.getTxIdsForBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', routes.getBlockHeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', routes.getAddress)
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', routes.getAddressTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs/chain/:txId', routes.getAddressTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', routes.getAddressPrefix)
;
}
if (Common.isLiquid()) {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'assets/icons', routes.getAllLiquidIcon)
.get(config.MEMPOOL.API_URL_PREFIX + 'assets/featured', routes.$getAllFeaturedLiquidAssets)
.get(config.MEMPOOL.API_URL_PREFIX + 'asset/:assetId/icon', routes.getLiquidIcon)
.get(config.MEMPOOL.API_URL_PREFIX + 'assets/group/:id', routes.$getAssetGroup)
;
liquidRoutes.initRoutes(this.app);
}
if (Common.isLiquid() && config.DATABASE.ENABLED) {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', routes.$getElementsPegsByMonth)
;
if (config.LIGHTNING.ENABLED) {
generalLightningRoutes.initRoutes(this.app);
nodesRoutes.initRoutes(this.app);
channelsRoutes.initRoutes(this.app);
}
}
}

View File

@@ -1,16 +1,17 @@
import { Common } from './api/common';
import blocks from './api/blocks';
import mempool from './api/mempool';
import mining from './api/mining';
import mining from './api/mining/mining';
import logger from './logger';
import HashratesRepository from './repositories/HashratesRepository';
import bitcoinClient from './api/bitcoin/bitcoin-client';
import priceUpdater from './tasks/price-updater';
import PricesRepository from './repositories/PricesRepository';
class Indexer {
runIndexer = true;
indexerRunning = false;
constructor() {
}
tasksRunning: string[] = [];
public reindex() {
if (Common.indexingEnabled()) {
@@ -18,6 +19,28 @@ class Indexer {
}
}
public async runSingleTask(task: 'blocksPrices') {
if (!Common.indexingEnabled()) {
return;
}
if (task === 'blocksPrices' && !this.tasksRunning.includes(task)) {
this.tasksRunning.push(task);
const lastestPriceId = await PricesRepository.$getLatestPriceId();
if (priceUpdater.historyInserted === false || lastestPriceId === null) {
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`)
setTimeout(() => {
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask != task)
this.runSingleTask('blocksPrices');
}, 10000);
} else {
logger.debug(`Blocks prices indexer will run now`)
await mining.$indexBlockPrices();
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask != task)
}
}
}
public async $run() {
if (!Common.indexingEnabled() || this.runIndexer === false ||
this.indexerRunning === true || mempool.hasPriority()
@@ -25,20 +48,48 @@ class Indexer {
return;
}
// Do not attempt to index anything unless Bitcoin Core is fully synced
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
if (blockchainInfo.blocks !== blockchainInfo.headers) {
return;
}
this.runIndexer = false;
this.indexerRunning = true;
logger.debug(`Running mining indexer`);
try {
await blocks.$generateBlockDatabase();
await this.$resetHashratesIndexingState();
await priceUpdater.$run();
const chainValid = await blocks.$generateBlockDatabase();
if (chainValid === false) {
// Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration
logger.warn(`The chain of block hash is invalid, re-indexing invalid data in 10 seconds.`);
setTimeout(() => this.reindex(), 10000);
this.indexerRunning = false;
return;
}
this.runSingleTask('blocksPrices');
await mining.$indexDifficultyAdjustments();
await this.$resetHashratesIndexingState(); // TODO - Remove this as it's not efficient
await mining.$generateNetworkHashrateHistory();
await mining.$generatePoolHashrateHistory();
await blocks.$generateBlocksSummariesDatabase();
} catch (e) {
this.reindex();
logger.err(`Indexer failed, trying again later. Reason: ` + (e instanceof Error ? e.message : e));
this.indexerRunning = false;
logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
setTimeout(() => this.reindex(), 10000);
this.indexerRunning = false;
return;
}
this.indexerRunning = false;
const runEvery = 1000 * 3600; // 1 hour
logger.debug(`Indexing completed. Next run planned at ${new Date(new Date().getTime() + runEvery).toUTCString()}`);
setTimeout(() => this.reindex(), runEvery);
}
async $resetHashratesIndexingState() {
@@ -47,6 +98,7 @@ class Indexer {
await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', 0);
} catch (e) {
logger.err(`Cannot reset hashrate indexing timestamps. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
}

View File

@@ -73,6 +73,9 @@ class Logger {
}
private getNetwork(): string {
if (config.LIGHTNING.ENABLED) {
return config.MEMPOOL.NETWORK === 'mainnet' ? 'lightning' : `${config.MEMPOOL.NETWORK}-lightning`;
}
if (config.BISQ.ENABLED) {
return 'bisq';
}

View File

@@ -22,6 +22,15 @@ export interface PoolStats extends PoolInfo {
emptyBlocks: number;
}
export interface BlockAudit {
time: number,
height: number,
hash: string,
missingTxs: string[],
addedTxs: string[],
matchRate: number,
}
export interface MempoolBlock {
blockSize: number;
blockVSize: number;
@@ -100,12 +109,23 @@ export interface BlockExtension {
avgFee?: number;
avgFeeRate?: number;
coinbaseRaw?: string;
usd?: number | null;
}
export interface BlockExtended extends IEsploraApi.Block {
extras: BlockExtension;
}
export interface BlockSummary {
id: string;
transactions: TransactionStripped[];
}
export interface BlockPrice {
height: number;
priceId: number;
}
export interface TransactionMinerInfo {
vin: VinStrippedToScriptsig[];
vout: VoutStrippedToScriptPubkey[];
@@ -219,8 +239,53 @@ export interface IDifficultyAdjustment {
timeOffset: number;
}
export interface IndexedDifficultyAdjustment {
time: number; // UNIX timestamp
height: number; // Block height
difficulty: number;
adjustment: number;
}
export interface RewardStats {
totalReward: number;
totalFee: number;
totalTx: number;
}
export interface ITopNodesPerChannels {
publicKey: string,
alias: string,
channels?: number,
capacity: number,
firstSeen?: number,
updatedAt?: number,
city?: any,
country?: any,
}
export interface ITopNodesPerCapacity {
publicKey: string,
alias: string,
capacity: number,
channels?: number,
firstSeen?: number,
updatedAt?: number,
city?: any,
country?: any,
}
export interface INodesRanking {
topByCapacity: ITopNodesPerCapacity[];
topByChannels: ITopNodesPerChannels[];
}
export interface IOldestNodes {
publicKey: string,
alias: string,
firstSeen: number,
channels?: number,
capacity: number,
updatedAt?: number,
city?: any,
country?: any,
}

View File

@@ -0,0 +1,75 @@
import DB from '../database';
import logger from '../logger';
import { BlockAudit } from '../mempool.interfaces';
class BlocksAuditRepositories {
public async $saveAudit(audit: BlockAudit): Promise<void> {
try {
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, match_rate)
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
JSON.stringify(audit.addedTxs), audit.matchRate]);
} catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
} else {
logger.err(`Cannot save block audit into db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
}
public async $getBlockPredictionsHistory(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT UNIX_TIMESTAMP(time) as time, height, match_rate FROM blocks_audits`;
if (interval !== null) {
query += ` WHERE time BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
query += ` GROUP BY UNIX_TIMESTAMP(time) DIV ${div} ORDER BY height`;
const [rows] = await DB.query(query);
return rows;
} catch (e: any) {
logger.err(`Cannot fetch block prediction history. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getPredictionsCount(): Promise<number> {
try {
const [rows] = await DB.query(`SELECT count(hash) as count FROM blocks_audits`);
return rows[0].count;
} catch (e: any) {
logger.err(`Cannot fetch block prediction history. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getBlockAudit(hash: string): Promise<any> {
try {
const [rows]: any[] = await DB.query(
`SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size,
blocks.weight, blocks.tx_count,
transactions, template, missing_txs as missingTxs, added_txs as addedTxs, match_rate as matchRate
FROM blocks_audits
JOIN blocks ON blocks.hash = blocks_audits.hash
JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash
WHERE blocks_audits.hash = "${hash}"
`);
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
rows[0].transactions = JSON.parse(rows[0].transactions);
rows[0].template = JSON.parse(rows[0].template);
return rows[0];
} catch (e: any) {
logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
}
export default new BlocksAuditRepositories();

View File

@@ -1,4 +1,4 @@
import { BlockExtended } from '../mempool.interfaces';
import { BlockExtended, BlockPrice } from '../mempool.interfaces';
import DB from '../database';
import logger from '../logger';
import { Common } from '../api/common';
@@ -6,6 +6,8 @@ import { prepareBlock } from '../utils/blocks-utils';
import PoolsRepository from './PoolsRepository';
import HashratesRepository from './HashratesRepository';
import { escape } from 'mysql2';
import BlocksSummariesRepository from './BlocksSummariesRepository';
import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository';
class BlocksRepository {
/**
@@ -254,7 +256,7 @@ class BlocksRepository {
const params: any[] = [];
let query = ` SELECT
height,
blocks.height,
hash as id,
UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp,
size,
@@ -306,7 +308,7 @@ class BlocksRepository {
public async $getBlockByHeight(height: number): Promise<object | null> {
try {
const [rows]: any[] = await DB.query(`SELECT
height,
blocks.height,
hash,
hash as id,
UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp,
@@ -334,7 +336,7 @@ class BlocksRepository {
avg_fee_rate
FROM blocks
JOIN pools ON blocks.pool_id = pools.id
WHERE height = ${height};
WHERE blocks.height = ${height}
`);
if (rows.length <= 0) {
@@ -355,15 +357,15 @@ class BlocksRepository {
public async $getBlockByHash(hash: string): Promise<object | null> {
try {
const query = `
SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, hash as id,
SELECT *, blocks.height, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, hash as id,
pools.id as pool_id, pools.name as pool_name, pools.link as pool_link, pools.slug as pool_slug,
pools.addresses as pool_addresses, pools.regexes as pool_regexes,
previous_block_hash as previousblockhash
FROM blocks
JOIN pools ON blocks.pool_id = pools.id
WHERE hash = '${hash}';
WHERE hash = ?;
`;
const [rows]: any[] = await DB.query(query);
const [rows]: any[] = await DB.query(query, [hash]);
if (rows.length <= 0) {
return null;
@@ -380,51 +382,25 @@ class BlocksRepository {
/**
* Return blocks difficulty
*/
public async $getBlocksDifficulty(interval: string | null): Promise<object[]> {
interval = Common.getSqlInterval(interval);
// :D ... Yeah don't ask me about this one https://stackoverflow.com/a/40303162
// Basically, using temporary user defined fields, we are able to extract all
// difficulty adjustments from the blocks tables.
// This allow use to avoid indexing it in another table.
let query = `
SELECT
*
FROM
(
SELECT
UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty, height,
IF(@prevStatus = YT.difficulty, @rn := @rn + 1,
IF(@prevStatus := YT.difficulty, @rn := 1, @rn := 1)
) AS rn
FROM blocks YT
CROSS JOIN
(
SELECT @prevStatus := -1, @rn := 1
) AS var
`;
if (interval) {
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
query += `
ORDER BY YT.height
) AS t
WHERE t.rn = 1
ORDER BY t.height
`;
public async $getBlocksDifficulty(): Promise<object[]> {
try {
const [rows]: any[] = await DB.query(query);
for (const row of rows) {
delete row['rn'];
}
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty FROM blocks`);
return rows;
} catch (e) {
logger.err('Cannot generate difficulty history. Reason: ' + (e instanceof Error ? e.message : e));
logger.err('Cannot get blocks difficulty list from the db. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Return blocks height
*/
public async $getBlocksHeightsAndTimestamp(): Promise<object[]> {
try {
const [rows]: any[] = await DB.query(`SELECT height, blockTimestamp as timestamp FROM blocks`);
return rows;
} catch (e) {
logger.err('Cannot get blocks height and timestamp from the db. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
@@ -451,26 +427,6 @@ class BlocksRepository {
}
}
/*
* Check if the last 10 blocks chain is valid
*/
public async $validateRecentBlocks(): Promise<boolean> {
try {
const [lastBlocks]: any[] = await DB.query(`SELECT height, hash, previous_block_hash FROM blocks ORDER BY height DESC LIMIT 10`);
for (let i = 0; i < lastBlocks.length - 1; ++i) {
if (lastBlocks[i].previous_block_hash !== lastBlocks[i + 1].hash) {
logger.warn(`Chain divergence detected at block ${lastBlocks[i].height}, re-indexing most recent data`);
return false;
}
}
return true;
} catch (e) {
return true; // Don't do anything if there is a db error
}
}
/**
* Check if the chain of block hash is valid and delete data from the stale branch if needed
*/
@@ -493,15 +449,17 @@ class BlocksRepository {
}
if (blocks[idx].previous_block_hash !== blocks[idx - 1].hash) {
logger.warn(`Chain divergence detected at block ${blocks[idx - 1].height}, re-indexing newer blocks and hashrates`);
logger.warn(`Chain divergence detected at block ${blocks[idx - 1].height}`);
await this.$deleteBlocksFrom(blocks[idx - 1].height);
await BlocksSummariesRepository.$deleteBlocksFrom(blocks[idx - 1].height);
await HashratesRepository.$deleteHashratesFromTimestamp(blocks[idx - 1].timestamp - 604800);
await DifficultyAdjustmentsRepository.$deleteAdjustementsFromHeight(blocks[idx - 1].height);
return false;
}
++idx;
}
logger.info(`${idx} blocks hash validated in ${new Date().getTime() - start} ms`);
logger.debug(`${idx} blocks hash validated in ${new Date().getTime() - start} ms`);
return true;
} catch (e) {
logger.err('Cannot validate chain of block hash. Reason: ' + (e instanceof Error ? e.message : e));
@@ -528,10 +486,14 @@ class BlocksRepository {
public async $getHistoricalBlockFees(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT
CAST(AVG(height) as INT) as avgHeight,
CAST(AVG(blocks.height) as INT) as avgHeight,
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
CAST(AVG(fees) as INT) as avgFees
FROM blocks`;
CAST(AVG(fees) as INT) as avgFees,
prices.USD
FROM blocks
JOIN blocks_prices on blocks_prices.height = blocks.height
JOIN prices on prices.id = blocks_prices.price_id
`;
if (interval !== null) {
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
@@ -553,10 +515,14 @@ class BlocksRepository {
public async $getHistoricalBlockRewards(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT
CAST(AVG(height) as INT) as avgHeight,
CAST(AVG(blocks.height) as INT) as avgHeight,
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
CAST(AVG(reward) as INT) as avgRewards
FROM blocks`;
CAST(AVG(reward) as INT) as avgRewards,
prices.USD
FROM blocks
JOIN blocks_prices on blocks_prices.height = blocks.height
JOIN prices on prices.id = blocks_prices.price_id
`;
if (interval !== null) {
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
@@ -652,6 +618,77 @@ class BlocksRepository {
throw e;
}
}
/**
* Get a list of blocks that have been indexed
*/
public async $getIndexedBlocks(): Promise<any[]> {
try {
const [rows]: any = await DB.query(`SELECT height, hash FROM blocks ORDER BY height DESC`);
return rows;
} catch (e) {
logger.err('Cannot generate block size and weight history. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Return the oldest block from a consecutive chain of block from the most recent one
*/
public async $getOldestConsecutiveBlock(): Promise<any> {
try {
const [rows]: any = await DB.query(`SELECT height, UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty FROM blocks ORDER BY height DESC`);
for (let i = 0; i < rows.length - 1; ++i) {
if (rows[i].height - rows[i + 1].height > 1) {
return rows[i];
}
}
return rows[rows.length - 1];
} catch (e) {
logger.err('Cannot generate block size and weight history. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get all blocks which have not be linked to a price yet
*/
public async $getBlocksWithoutPrice(): Promise<object[]> {
try {
const [rows]: any[] = await DB.query(`
SELECT UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.height
FROM blocks
LEFT JOIN blocks_prices ON blocks.height = blocks_prices.height
WHERE blocks_prices.height IS NULL
ORDER BY blocks.height
`);
return rows;
} catch (e) {
logger.err('Cannot get blocks height and timestamp from the db. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Save block price by batch
*/
public async $saveBlockPrices(blockPrices: BlockPrice[]): Promise<void> {
try {
let query = `INSERT INTO blocks_prices(height, price_id) VALUES`;
for (const price of blockPrices) {
query += ` (${price.height}, ${price.priceId}),`
}
query = query.slice(0, -1);
await DB.query(query);
} catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Cannot save blocks prices for blocks [${blockPrices[0].height} to ${blockPrices[blockPrices.length - 1].height}] because it has already been indexed, ignoring`);
} else {
logger.err(`Cannot save blocks prices for blocks [${blockPrices[0].height} to ${blockPrices[blockPrices.length - 1].height}] into db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
}
}
export default new BlocksRepository();

View File

@@ -0,0 +1,69 @@
import DB from '../database';
import logger from '../logger';
import { BlockSummary } from '../mempool.interfaces';
class BlocksSummariesRepository {
public async $getByBlockId(id: string): Promise<BlockSummary | undefined> {
try {
const [summary]: any[] = await DB.query(`SELECT * from blocks_summaries WHERE id = ?`, [id]);
if (summary.length > 0) {
summary[0].transactions = JSON.parse(summary[0].transactions);
return summary[0];
}
} catch (e) {
logger.err(`Cannot get block summary for block id ${id}. Reason: ` + (e instanceof Error ? e.message : e));
}
return undefined;
}
public async $saveSummary(params: { height: number, mined?: BlockSummary, template?: BlockSummary}) {
const blockId = params.mined?.id ?? params.template?.id;
try {
const [dbSummary]: any[] = await DB.query(`SELECT * FROM blocks_summaries WHERE id = "${blockId}"`);
if (dbSummary.length === 0) { // First insertion
await DB.query(`INSERT INTO blocks_summaries VALUE (?, ?, ?, ?)`, [
params.height, blockId, JSON.stringify(params.mined?.transactions ?? []), JSON.stringify(params.template?.transactions ?? [])
]);
} else if (params.mined !== undefined) { // Update mined block summary
await DB.query(`UPDATE blocks_summaries SET transactions = ? WHERE id = "${params.mined.id}"`, [JSON.stringify(params.mined.transactions)]);
} else if (params.template !== undefined) { // Update template block summary
await DB.query(`UPDATE blocks_summaries SET template = ? WHERE id = "${params.template.id}"`, [JSON.stringify(params.template?.transactions)]);
}
} catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Cannot save block summary for ${blockId} because it has already been indexed, ignoring`);
} else {
logger.debug(`Cannot save block summary for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
}
}
}
public async $getIndexedSummariesId(): Promise<string[]> {
try {
const [rows]: any[] = await DB.query(`SELECT id from blocks_summaries`);
return rows.map(row => row.id);
} catch (e) {
logger.err(`Cannot get block summaries id list. Reason: ` + (e instanceof Error ? e.message : e));
}
return [];
}
/**
* Delete blocks from the database from blockHeight
*/
public async $deleteBlocksFrom(blockHeight: number) {
logger.info(`Delete newer blocks summary from height ${blockHeight} from the database`);
try {
await DB.query(`DELETE FROM blocks_summaries where height >= ${blockHeight}`);
} catch (e) {
logger.err('Cannot delete indexed blocks summaries. Reason: ' + (e instanceof Error ? e.message : e));
}
}
}
export default new BlocksSummariesRepository();

View File

@@ -0,0 +1,124 @@
import { Common } from '../api/common';
import config from '../config';
import DB from '../database';
import logger from '../logger';
import { IndexedDifficultyAdjustment } from '../mempool.interfaces';
class DifficultyAdjustmentsRepository {
public async $saveAdjustments(adjustment: IndexedDifficultyAdjustment): Promise<void> {
if (adjustment.height === 1) {
return;
}
try {
const query = `INSERT INTO difficulty_adjustments(time, height, difficulty, adjustment) VALUE (FROM_UNIXTIME(?), ?, ?, ?)`;
const params: any[] = [
adjustment.time,
adjustment.height,
adjustment.difficulty,
adjustment.adjustment,
];
await DB.query(query, params);
} catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Cannot save difficulty adjustment at block ${adjustment.height}, already indexed, ignoring`);
} else {
logger.err(`Cannot save difficulty adjustment at block ${adjustment.height}. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
}
}
}
public async $getAdjustments(interval: string | null, descOrder: boolean = false): Promise<IndexedDifficultyAdjustment[]> {
interval = Common.getSqlInterval(interval);
let query = `SELECT
CAST(AVG(UNIX_TIMESTAMP(time)) as INT) as time,
CAST(AVG(height) AS INT) as height,
CAST(AVG(difficulty) as DOUBLE) as difficulty,
CAST(AVG(adjustment) as DOUBLE) as adjustment
FROM difficulty_adjustments`;
if (interval) {
query += ` WHERE time BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
query += ` GROUP BY UNIX_TIMESTAMP(time) DIV ${86400}`;
if (descOrder === true) {
query += ` ORDER BY height DESC`;
} else {
query += ` ORDER BY height`;
}
try {
const [rows] = await DB.query(query);
return rows as IndexedDifficultyAdjustment[];
} catch (e) {
logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getRawAdjustments(interval: string | null, descOrder: boolean = false): Promise<IndexedDifficultyAdjustment[]> {
interval = Common.getSqlInterval(interval);
let query = `SELECT
UNIX_TIMESTAMP(time) as time,
height as height,
difficulty as difficulty,
adjustment as adjustment
FROM difficulty_adjustments`;
if (interval) {
query += ` WHERE time BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
if (descOrder === true) {
query += ` ORDER BY height DESC`;
} else {
query += ` ORDER BY height`;
}
try {
const [rows] = await DB.query(query);
return rows as IndexedDifficultyAdjustment[];
} catch (e) {
logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getAdjustmentsHeights(): Promise<number[]> {
try {
const [rows]: any[] = await DB.query(`SELECT height FROM difficulty_adjustments`);
return rows.map(block => block.height);
} catch (e: any) {
logger.err(`Cannot get difficulty adjustment block heights. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
}
}
public async $deleteAdjustementsFromHeight(height: number): Promise<void> {
try {
logger.info(`Delete newer difficulty adjustments from height ${height} from the database`);
await DB.query(`DELETE FROM difficulty_adjustments WHERE height >= ?`, [height]);
} catch (e: any) {
logger.err(`Cannot delete difficulty adjustments from the database. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
}
}
public async $deleteLastAdjustment(): Promise<void> {
try {
logger.info(`Delete last difficulty adjustment from the database`);
await DB.query(`DELETE FROM difficulty_adjustments ORDER BY time LIMIT 1`);
} catch (e: any) {
logger.err(`Cannot delete last difficulty adjustment from the database. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
}
}
}
export default new DifficultyAdjustmentsRepository();

View File

@@ -29,10 +29,12 @@ class HashratesRepository {
}
}
public async $getNetworkDailyHashrate(interval: string | null): Promise<any[]> {
public async $getRawNetworkDailyHashrate(interval: string | null): Promise<any[]> {
interval = Common.getSqlInterval(interval);
let query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate
let query = `SELECT
UNIX_TIMESTAMP(hashrate_timestamp) as timestamp,
avg_hashrate as avgHashrate
FROM hashrates`;
if (interval) {
@@ -53,6 +55,33 @@ class HashratesRepository {
}
}
public async $getNetworkDailyHashrate(interval: string | null): Promise<any[]> {
interval = Common.getSqlInterval(interval);
let query = `SELECT
CAST(AVG(UNIX_TIMESTAMP(hashrate_timestamp)) as INT) as timestamp,
CAST(AVG(avg_hashrate) as DOUBLE) as avgHashrate
FROM hashrates`;
if (interval) {
query += ` WHERE hashrate_timestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()
AND hashrates.type = 'daily'`;
} else {
query += ` WHERE hashrates.type = 'daily'`;
}
query += ` GROUP BY UNIX_TIMESTAMP(hashrate_timestamp) DIV ${86400}`;
query += ` ORDER by hashrate_timestamp`;
try {
const [rows]: any[] = await DB.query(query);
return rows;
} catch (e) {
logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getWeeklyHashrateTimestamps(): Promise<number[]> {
const query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp
FROM hashrates
@@ -75,6 +104,9 @@ class HashratesRepository {
interval = Common.getSqlInterval(interval);
const topPoolsId = (await PoolsRepository.$getPoolsInfo('1w')).map((pool) => pool.poolId);
if (topPoolsId.length === 0) {
return [];
}
let query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate, share, pools.name as poolName
FROM hashrates

View File

@@ -0,0 +1,45 @@
import { ResultSetHeader } from 'mysql2';
import DB from '../database';
import logger from '../logger';
export interface NodeSocket {
publicKey: string;
network: string | null;
addr: string;
}
class NodesSocketsRepository {
public async $saveSocket(socket: NodeSocket): Promise<void> {
try {
await DB.query(`
INSERT INTO nodes_sockets(public_key, socket, type)
VALUE (?, ?, ?)
`, [socket.publicKey, socket.addr, socket.network]);
} catch (e: any) {
if (e.errno !== 1062) { // ER_DUP_ENTRY - Not an issue, just ignore this
logger.err(`Cannot save node socket (${[socket.publicKey, socket.addr, socket.network]}) into db. Reason: ` + (e instanceof Error ? e.message : e));
// We don't throw, not a critical issue if we miss some nodes sockets
}
}
}
public async $deleteUnusedSockets(publicKey: string, addresses: string[]): Promise<number> {
if (addresses.length === 0) {
return 0;
}
try {
const query = `
DELETE FROM nodes_sockets
WHERE public_key = ?
AND socket NOT IN (${addresses.map(id => `"${id}"`).join(',')})
`;
const [result] = await DB.query<ResultSetHeader>(query, [publicKey]);
return result.affectedRows;
} catch (e) {
logger.err(`Cannot delete unused sockets for ${publicKey} from db. Reason: ` + (e instanceof Error ? e.message : e));
return 0;
}
}
}
export default new NodesSocketsRepository();

View File

@@ -0,0 +1,52 @@
import DB from '../database';
import logger from '../logger';
import { Prices } from '../tasks/price-updater';
class PricesRepository {
public async $savePrices(time: number, prices: Prices): Promise<void> {
if (prices.USD === -1) {
// Some historical price entries have not USD prices, so we just ignore them to avoid future UX issues
// As of today there are only 4 (on 2013-09-05, 2013-09-19, 2013-09-12 and 2013-09-26) so that's fine
return;
}
try {
await DB.query(`
INSERT INTO prices(time, USD, EUR, GBP, CAD, CHF, AUD, JPY)
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ? )`,
[time, prices.USD, prices.EUR, prices.GBP, prices.CAD, prices.CHF, prices.AUD, prices.JPY]
);
} catch (e: any) {
logger.err(`Cannot save exchange rate into db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getOldestPriceTime(): Promise<number> {
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time LIMIT 1`);
return oldestRow[0] ? oldestRow[0].time : 0;
}
public async $getLatestPriceId(): Promise<number | null> {
const [oldestRow] = await DB.query(`SELECT id from prices WHERE USD != -1 ORDER BY time DESC LIMIT 1`);
return oldestRow[0] ? oldestRow[0].id : null;
}
public async $getLatestPriceTime(): Promise<number> {
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time DESC LIMIT 1`);
return oldestRow[0] ? oldestRow[0].time : 0;
}
public async $getPricesTimes(): Promise<number[]> {
const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time`);
return times.map(time => time.time);
}
public async $getPricesTimesAndId(): Promise<number[]> {
const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time, id, USD from prices ORDER BY time`);
return times;
}
}
export default new PricesRepository();

View File

@@ -1,21 +0,0 @@
import DB from '../database';
import logger from '../logger';
import { IConversionRates } from '../mempool.interfaces';
class RatesRepository {
public async $saveRate(height: number, rates: IConversionRates) {
try {
await DB.query(`INSERT INTO rates(height, bisq_rates) VALUE (?, ?)`, [height, JSON.stringify(rates)]);
} catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Rate already exists for block ${height}, ignoring`);
} else {
logger.err(`Cannot save exchange rate into db for block ${height} Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
}
}
export default new RatesRepository();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,431 @@
import DB from '../../database';
import logger from '../../logger';
import channelsApi from '../../api/explorer/channels.api';
import bitcoinApi from '../../api/bitcoin/bitcoin-api-factory';
import config from '../../config';
import { IEsploraApi } from '../../api/bitcoin/esplora-api.interface';
import { ILightningApi } from '../../api/lightning/lightning-api.interface';
import { $lookupNodeLocation } from './sync-tasks/node-locations';
import lightningApi from '../../api/lightning/lightning-api-factory';
import nodesApi from '../../api/explorer/nodes.api';
import { ResultSetHeader } from 'mysql2';
import fundingTxFetcher from './sync-tasks/funding-tx-fetcher';
import NodesSocketsRepository from '../../repositories/NodesSocketsRepository';
import { Common } from '../../api/common';
import blocks from '../../api/blocks';
class NetworkSyncService {
loggerTimer = 0;
closedChannelsScanBlock = 0;
constructor() {}
public async $startService(): Promise<void> {
logger.info('Starting lightning network sync service');
this.loggerTimer = new Date().getTime() / 1000;
await this.$runTasks();
}
private async $runTasks(): Promise<void> {
try {
logger.info(`Updating nodes and channels`);
const networkGraph = await lightningApi.$getNetworkGraph();
if (networkGraph.nodes.length === 0 || networkGraph.edges.length === 0) {
logger.info(`LN Network graph is empty, retrying in 10 seconds`);
setTimeout(() => { this.$runTasks(); }, 10000);
return;
}
await this.$updateNodesList(networkGraph.nodes);
await this.$updateChannelsList(networkGraph.edges);
await this.$deactivateChannelsWithoutActiveNodes();
await this.$lookUpCreationDateFromChain();
await this.$updateNodeFirstSeen();
await this.$scanForClosedChannels();
if (config.MEMPOOL.BACKEND === 'esplora') {
await this.$runClosedChannelsForensics();
}
} catch (e) {
logger.err('$runTasks() error: ' + (e instanceof Error ? e.message : e));
}
setTimeout(() => { this.$runTasks(); }, 1000 * config.LIGHTNING.GRAPH_REFRESH_INTERVAL);
}
/**
* Update the `nodes` table to reflect the current network graph state
*/
private async $updateNodesList(nodes: ILightningApi.Node[]): Promise<void> {
let progress = 0;
let deletedSockets = 0;
const graphNodesPubkeys: string[] = [];
for (const node of nodes) {
const latestUpdated = await channelsApi.$getLatestChannelUpdateForNode(node.pub_key);
node.last_update = Math.max(node.last_update, latestUpdated);
await nodesApi.$saveNode(node);
graphNodesPubkeys.push(node.pub_key);
++progress;
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
if (elapsedSeconds > 10) {
logger.info(`Updating node ${progress}/${nodes.length}`);
this.loggerTimer = new Date().getTime() / 1000;
}
const addresses: string[] = [];
for (const socket of node.addresses) {
await NodesSocketsRepository.$saveSocket(Common.formatSocket(node.pub_key, socket));
addresses.push(socket.addr);
}
deletedSockets += await NodesSocketsRepository.$deleteUnusedSockets(node.pub_key, addresses);
}
logger.info(`${progress} nodes updated. ${deletedSockets} sockets deleted`);
// If a channel if not present in the graph, mark it as inactive
await nodesApi.$setNodesInactive(graphNodesPubkeys);
if (config.MAXMIND.ENABLED) {
$lookupNodeLocation();
}
}
/**
* Update the `channels` table to reflect the current network graph state
*/
private async $updateChannelsList(channels: ILightningApi.Channel[]): Promise<void> {
try {
const [closedChannelsRaw]: any[] = await DB.query(`SELECT id FROM channels WHERE status = 2`);
const closedChannels = {};
for (const closedChannel of closedChannelsRaw) {
closedChannels[closedChannel.id] = true;
}
let progress = 0;
const graphChannelsIds: string[] = [];
for (const channel of channels) {
if (!closedChannels[channel.channel_id]) {
await channelsApi.$saveChannel(channel);
}
graphChannelsIds.push(channel.channel_id);
++progress;
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
if (elapsedSeconds > 10) {
logger.info(`Updating channel ${progress}/${channels.length}`);
this.loggerTimer = new Date().getTime() / 1000;
}
}
logger.info(`${progress} channels updated`);
// If a channel if not present in the graph, mark it as inactive
await channelsApi.$setChannelsInactive(graphChannelsIds);
} catch (e) {
logger.err(`Cannot update channel list. Reason: ${(e instanceof Error ? e.message : e)}`);
}
}
// This method look up the creation date of the earliest channel of the node
// and update the node to that date in order to get the earliest first seen date
private async $updateNodeFirstSeen(): Promise<void> {
let progress = 0;
let updated = 0;
try {
const [nodes]: any[] = await DB.query(`
SELECT nodes.public_key, UNIX_TIMESTAMP(nodes.first_seen) AS first_seen,
(
SELECT MIN(UNIX_TIMESTAMP(created))
FROM channels
WHERE channels.node1_public_key = nodes.public_key
) AS created1,
(
SELECT MIN(UNIX_TIMESTAMP(created))
FROM channels
WHERE channels.node2_public_key = nodes.public_key
) AS created2
FROM nodes
`);
for (const node of nodes) {
const lowest = Math.min(
node.created1 ?? Number.MAX_SAFE_INTEGER,
node.created2 ?? Number.MAX_SAFE_INTEGER,
node.first_seen ?? Number.MAX_SAFE_INTEGER
);
if (lowest < node.first_seen) {
const query = `UPDATE nodes SET first_seen = FROM_UNIXTIME(?) WHERE public_key = ?`;
const params = [lowest, node.public_key];
await DB.query(query, params);
}
++progress;
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
if (elapsedSeconds > 10) {
logger.info(`Updating node first seen date ${progress}/${nodes.length}`);
this.loggerTimer = new Date().getTime() / 1000;
++updated;
}
}
logger.info(`Updated ${updated} node first seen dates`);
} catch (e) {
logger.err('$updateNodeFirstSeen() error: ' + (e instanceof Error ? e.message : e));
}
}
private async $lookUpCreationDateFromChain(): Promise<void> {
let progress = 0;
logger.info(`Running channel creation date lookup`);
try {
const channels = await channelsApi.$getChannelsWithoutCreatedDate();
for (const channel of channels) {
const transaction = await fundingTxFetcher.$fetchChannelOpenTx(channel.short_id);
await DB.query(`
UPDATE channels SET created = FROM_UNIXTIME(?) WHERE channels.id = ?`,
[transaction.timestamp, channel.id]
);
++progress;
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
if (elapsedSeconds > 10) {
logger.info(`Updating channel creation date ${progress}/${channels.length}`);
this.loggerTimer = new Date().getTime() / 1000;
}
}
logger.info(`Updated ${channels.length} channels' creation date`);
} catch (e) {
logger.err('$lookUpCreationDateFromChain() error: ' + (e instanceof Error ? e.message : e));
}
}
/**
* If a channel does not have any active node linked to it, then also
* mark that channel as inactive
*/
private async $deactivateChannelsWithoutActiveNodes(): Promise<void> {
logger.info(`Find channels which nodes are offline`);
try {
const result = await DB.query<ResultSetHeader>(`
UPDATE channels
SET status = 0
WHERE channels.status = 1
AND (
(
SELECT COUNT(*)
FROM nodes
WHERE nodes.public_key = channels.node1_public_key
AND nodes.status = 1
) = 0
OR (
SELECT COUNT(*)
FROM nodes
WHERE nodes.public_key = channels.node2_public_key
AND nodes.status = 1
) = 0)
`);
if (result[0].changedRows ?? 0 > 0) {
logger.info(`Marked ${result[0].changedRows} channels as inactive because they are not linked to any active node`);
} else {
logger.debug(`Marked ${result[0].changedRows} channels as inactive because they are not linked to any active node`);
}
} catch (e) {
logger.err('$deactivateChannelsWithoutActiveNodes() error: ' + (e instanceof Error ? e.message : e));
}
}
private async $scanForClosedChannels(): Promise<void> {
if (this.closedChannelsScanBlock === blocks.getCurrentBlockHeight()) {
logger.debug(`We've already scan closed channels for this block, skipping.`);
return;
}
let progress = 0;
try {
let log = `Starting closed channels scan`;
if (this.closedChannelsScanBlock > 0) {
log += `. Last scan was at block ${this.closedChannelsScanBlock}`;
} else {
log += ` for the first time`;
}
logger.info(log);
const channels = await channelsApi.$getChannelsByStatus([0, 1]);
for (const channel of channels) {
const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout);
if (spendingTx.spent === true && spendingTx.status?.confirmed === true) {
logger.debug('Marking channel: ' + channel.id + ' as closed.');
await DB.query(`UPDATE channels SET status = 2, closing_date = FROM_UNIXTIME(?) WHERE id = ?`,
[spendingTx.status.block_time, channel.id]);
if (spendingTx.txid && !channel.closing_transaction_id) {
await DB.query(`UPDATE channels SET closing_transaction_id = ? WHERE id = ?`, [spendingTx.txid, channel.id]);
}
}
++progress;
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
if (elapsedSeconds > 10) {
logger.info(`Checking if channel has been closed ${progress}/${channels.length}`);
this.loggerTimer = new Date().getTime() / 1000;
}
}
this.closedChannelsScanBlock = blocks.getCurrentBlockHeight();
logger.info(`Closed channels scan completed at block ${this.closedChannelsScanBlock}`);
} catch (e) {
logger.err('$scanForClosedChannels() error: ' + (e instanceof Error ? e.message : e));
}
}
/*
1. Mutually closed
2. Forced closed
3. Forced closed with penalty
*/
private async $runClosedChannelsForensics(): Promise<void> {
if (!config.ESPLORA.REST_API_URL) {
return;
}
let progress = 0;
try {
logger.info(`Started running closed channel forensics...`);
const channels = await channelsApi.$getClosedChannelsWithoutReason();
for (const channel of channels) {
let reason = 0;
// Only Esplora backend can retrieve spent transaction outputs
try {
let outspends: IEsploraApi.Outspend[] | undefined;
try {
outspends = await bitcoinApi.$getOutspends(channel.closing_transaction_id);
} catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + channel.closing_transaction_id + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
continue;
}
const lightningScriptReasons: number[] = [];
for (const outspend of outspends) {
if (outspend.spent && outspend.txid) {
let spendingTx: IEsploraApi.Transaction | undefined;
try {
spendingTx = await bitcoinApi.$getRawTransaction(outspend.txid);
} catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + outspend.txid}. Reason ${e instanceof Error ? e.message : e}`);
continue;
}
const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
lightningScriptReasons.push(lightningScript);
}
}
if (lightningScriptReasons.length === outspends.length
&& lightningScriptReasons.filter((r) => r === 1).length === outspends.length) {
reason = 1;
} else {
const filteredReasons = lightningScriptReasons.filter((r) => r !== 1);
if (filteredReasons.length) {
if (filteredReasons.some((r) => r === 2 || r === 4)) {
reason = 3;
} else {
reason = 2;
}
} else {
/*
We can detect a commitment transaction (force close) by reading Sequence and Locktime
https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction
*/
let closingTx: IEsploraApi.Transaction | undefined;
try {
closingTx = await bitcoinApi.$getRawTransaction(channel.closing_transaction_id);
} catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + channel.closing_transaction_id}. Reason ${e instanceof Error ? e.message : e}`);
continue;
}
const sequenceHex: string = closingTx.vin[0].sequence.toString(16);
const locktimeHex: string = closingTx.locktime.toString(16);
if (sequenceHex.substring(0, 2) === '80' && locktimeHex.substring(0, 2) === '20') {
reason = 2; // Here we can't be sure if it's a penalty or not
} else {
reason = 1;
}
}
}
if (reason) {
logger.debug('Setting closing reason ' + reason + ' for channel: ' + channel.id + '.');
await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
}
} catch (e) {
logger.err(`$runClosedChannelsForensics() failed for channel ${channel.short_id}. Reason: ${e instanceof Error ? e.message : e}`);
}
++progress;
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
if (elapsedSeconds > 10) {
logger.info(`Updating channel closed channel forensics ${progress}/${channels.length}`);
this.loggerTimer = new Date().getTime() / 1000;
}
}
logger.info(`Closed channels forensics scan complete.`);
} catch (e) {
logger.err('$runClosedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
}
}
private findLightningScript(vin: IEsploraApi.Vin): number {
const topElement = vin.witness[vin.witness.length - 2];
if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(vin.inner_witnessscript_asm)) {
// https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
if (topElement === '01') {
// top element is '01' to get in the revocation path
// 'Revoked Lightning Force Close';
// Penalty force closed
return 2;
} else {
// top element is '', this is a delayed to_local output
// 'Lightning Force Close';
return 3;
}
} else if (
/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(vin.inner_witnessscript_asm) ||
/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_IF OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_DROP OP_PUSHBYTES_3 \w{6} OP_CLTV OP_DROP OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(vin.inner_witnessscript_asm)
) {
// https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
// https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs
if (topElement.length === 66) {
// top element is a public key
// 'Revoked Lightning HTLC'; Penalty force closed
return 4;
} else if (topElement) {
// top element is a preimage
// 'Lightning HTLC';
return 5;
} else {
// top element is '' to get in the expiry of the script
// 'Expired Lightning HTLC';
return 6;
}
} else if (/^OP_PUSHBYTES_33 \w{66} OP_CHECKSIG OP_IFDUP OP_NOTIF OP_PUSHNUM_16 OP_CSV OP_ENDIF$/.test(vin.inner_witnessscript_asm)) {
// https://github.com/lightning/bolts/blob/master/03-transactions.md#to_local_anchor-and-to_remote_anchor-output-option_anchors
if (topElement) {
// top element is a signature
// 'Lightning Anchor';
return 7;
} else {
// top element is '', it has been swept after 16 blocks
// 'Swept Lightning Anchor';
return 8;
}
}
return 1;
}
}
export default new NetworkSyncService();

View File

@@ -0,0 +1,34 @@
import logger from '../../logger';
import lightningApi from '../../api/lightning/lightning-api-factory';
import LightningStatsImporter from './sync-tasks/stats-importer';
import config from '../../config';
import { Common } from '../../api/common';
class LightningStatsUpdater {
public async $startService(): Promise<void> {
logger.info('Starting Lightning Stats service');
await this.$runTasks();
LightningStatsImporter.$run();
}
private async $runTasks(): Promise<void> {
await this.$logStatsDaily();
setTimeout(() => { this.$runTasks(); }, 1000 * config.LIGHTNING.STATS_REFRESH_INTERVAL);
}
/**
* Update the latest entry for each node every config.LIGHTNING.STATS_REFRESH_INTERVAL seconds
*/
private async $logStatsDaily(): Promise<void> {
const date = new Date();
Common.setDateMidnight(date);
const networkGraph = await lightningApi.$getNetworkGraph();
await LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph);
logger.info(`Updated latest network stats`);
}
}
export default new LightningStatsUpdater();

View File

@@ -0,0 +1,116 @@
import { existsSync, promises } from 'fs';
import bitcoinClient from '../../../api/bitcoin/bitcoin-client';
import { Common } from '../../../api/common';
import config from '../../../config';
import logger from '../../../logger';
const fsPromises = promises;
const BLOCKS_CACHE_MAX_SIZE = 100;
const CACHE_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/ln-funding-txs-cache.json';
class FundingTxFetcher {
private running = false;
private blocksCache = {};
private channelNewlyProcessed = 0;
public fundingTxCache = {};
async $init(): Promise<void> {
// Load funding tx disk cache
if (Object.keys(this.fundingTxCache).length === 0 && existsSync(CACHE_FILE_NAME)) {
try {
this.fundingTxCache = JSON.parse(await fsPromises.readFile(CACHE_FILE_NAME, 'utf-8'));
} catch (e) {
logger.err(`Unable to parse channels funding txs disk cache. Starting from scratch`);
this.fundingTxCache = {};
}
logger.debug(`Imported ${Object.keys(this.fundingTxCache).length} funding tx amount from the disk cache`);
}
}
async $fetchChannelsFundingTxs(channelIds: string[]): Promise<void> {
if (this.running) {
return;
}
this.running = true;
const globalTimer = new Date().getTime() / 1000;
let cacheTimer = new Date().getTime() / 1000;
let loggerTimer = new Date().getTime() / 1000;
let channelProcessed = 0;
this.channelNewlyProcessed = 0;
for (const channelId of channelIds) {
await this.$fetchChannelOpenTx(channelId);
++channelProcessed;
let elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
if (elapsedSeconds > 10) {
elapsedSeconds = Math.round((new Date().getTime() / 1000) - globalTimer);
logger.info(`Indexing channels funding tx ${channelProcessed + 1} of ${channelIds.length} ` +
`(${Math.floor(channelProcessed / channelIds.length * 10000) / 100}%) | ` +
`elapsed: ${elapsedSeconds} seconds`
);
loggerTimer = new Date().getTime() / 1000;
}
elapsedSeconds = Math.round((new Date().getTime() / 1000) - cacheTimer);
if (elapsedSeconds > 60) {
logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`);
fsPromises.writeFile(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache));
cacheTimer = new Date().getTime() / 1000;
}
}
if (this.channelNewlyProcessed > 0) {
logger.info(`Indexed ${this.channelNewlyProcessed} additional channels funding tx`);
logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`);
fsPromises.writeFile(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache));
}
this.running = false;
}
public async $fetchChannelOpenTx(channelId: string): Promise<{timestamp: number, txid: string, value: number}> {
channelId = Common.channelIntegerIdToShortId(channelId);
if (this.fundingTxCache[channelId]) {
return this.fundingTxCache[channelId];
}
const parts = channelId.split('x');
const blockHeight = parts[0];
const txIdx = parts[1];
const outputIdx = parts[2];
let block = this.blocksCache[blockHeight];
// Fetch it from core
if (!block) {
const blockHash = await bitcoinClient.getBlockHash(parseInt(blockHeight, 10));
block = await bitcoinClient.getBlock(blockHash, 1);
}
this.blocksCache[block.height] = block;
const blocksCacheHashes = Object.keys(this.blocksCache).sort((a, b) => parseInt(b) - parseInt(a)).reverse();
if (blocksCacheHashes.length > BLOCKS_CACHE_MAX_SIZE) {
for (let i = 0; i < 10; ++i) {
delete this.blocksCache[blocksCacheHashes[i]];
}
}
const txid = block.tx[txIdx];
const rawTx = await bitcoinClient.getRawTransaction(txid);
const tx = await bitcoinClient.decodeRawTransaction(rawTx);
this.fundingTxCache[channelId] = {
timestamp: block.time,
txid: txid,
value: tx.vout[outputIdx].value,
};
++this.channelNewlyProcessed;
return this.fundingTxCache[channelId];
}
}
export default new FundingTxFetcher;

View File

@@ -0,0 +1,164 @@
import * as net from 'net';
import maxmind, { CityResponse, AsnResponse, IspResponse } from 'maxmind';
import nodesApi from '../../../api/explorer/nodes.api';
import config from '../../../config';
import DB from '../../../database';
import logger from '../../../logger';
import { ResultSetHeader } from 'mysql2';
import * as IPCheck from '../../../utils/ipcheck.js';
export async function $lookupNodeLocation(): Promise<void> {
let loggerTimer = new Date().getTime() / 1000;
let progress = 0;
let nodesUpdated = 0;
let geoNamesInserted = 0;
logger.info(`Running node location updater using Maxmind`);
try {
const nodes = await nodesApi.$getAllNodes();
const lookupCity = await maxmind.open<CityResponse>(config.MAXMIND.GEOLITE2_CITY);
const lookupAsn = await maxmind.open<AsnResponse>(config.MAXMIND.GEOLITE2_ASN);
const lookupIsp = await maxmind.open<IspResponse>(config.MAXMIND.GEOIP2_ISP);
for (const node of nodes) {
const sockets: string[] = node.sockets.split(',');
for (const socket of sockets) {
const ip = socket.substring(0, socket.lastIndexOf(':')).replace('[', '').replace(']', '');
const hasClearnet = [4, 6].includes(net.isIP(ip));
if (hasClearnet && ip !== '127.0.1.1' && ip !== '127.0.0.1') {
const city = lookupCity.get(ip);
const asn = lookupAsn.get(ip);
const isp = lookupIsp.get(ip);
let asOverwrite: any | undefined;
if (asn && (IPCheck.match(ip, '170.75.160.0/20') || IPCheck.match(ip, '172.81.176.0/21'))) {
asOverwrite = {
asn: 394745,
name: 'Lunanode',
};
}
else if (asn && (IPCheck.match(ip, '50.7.0.0/16') || IPCheck.match(ip, '66.90.64.0/18'))) {
asOverwrite = {
asn: 30058,
name: 'FDCservers.net',
};
}
else if (asn && asn.autonomous_system_number === 174) {
asOverwrite = {
asn: 174,
name: 'Cogent Communications',
};
}
if (city && (asn || isp)) {
const query = `
UPDATE nodes SET
as_number = ?,
city_id = ?,
country_id = ?,
subdivision_id = ?,
longitude = ?,
latitude = ?,
accuracy_radius = ?
WHERE public_key = ?
`;
const params = [
asOverwrite?.asn ?? isp?.autonomous_system_number ?? asn?.autonomous_system_number,
city.city?.geoname_id,
city.country?.geoname_id,
city.subdivisions ? city.subdivisions[0].geoname_id : null,
city.location?.longitude,
city.location?.latitude,
city.location?.accuracy_radius,
node.public_key
];
let result = await DB.query<ResultSetHeader>(query, params);
if (result[0].changedRows ?? 0 > 0) {
++nodesUpdated;
}
// Store Continent
if (city.continent?.geoname_id) {
result = await DB.query<ResultSetHeader>(
`INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'continent', ?)`,
[city.continent?.geoname_id, JSON.stringify(city.continent?.names)]);
if (result[0].changedRows ?? 0 > 0) {
++geoNamesInserted;
}
}
// Store Country
if (city.country?.geoname_id) {
result = await DB.query<ResultSetHeader>(
`INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'country', ?)`,
[city.country?.geoname_id, JSON.stringify(city.country?.names)]);
if (result[0].changedRows ?? 0 > 0) {
++geoNamesInserted;
}
}
// Store Country ISO code
if (city.country?.iso_code) {
result = await DB.query<ResultSetHeader>(
`INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'country_iso_code', ?)`,
[city.country?.geoname_id, city.country?.iso_code]);
if (result[0].changedRows ?? 0 > 0) {
++geoNamesInserted;
}
}
// Store Division
if (city.subdivisions && city.subdivisions[0]) {
result = await DB.query<ResultSetHeader>(
`INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'division', ?)`,
[city.subdivisions[0].geoname_id, JSON.stringify(city.subdivisions[0]?.names)]);
if (result[0].changedRows ?? 0 > 0) {
++geoNamesInserted;
}
}
// Store City
if (city.city?.geoname_id) {
result = await DB.query<ResultSetHeader>(
`INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'city', ?)`,
[city.city?.geoname_id, JSON.stringify(city.city?.names)]);
if (result[0].changedRows ?? 0 > 0) {
++geoNamesInserted;
}
}
// Store AS name
if (isp?.autonomous_system_organization ?? asn?.autonomous_system_organization) {
result = await DB.query<ResultSetHeader>(
`INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'as_organization', ?)`,
[
asOverwrite?.asn ?? isp?.autonomous_system_number ?? asn?.autonomous_system_number,
JSON.stringify(asOverwrite?.name ?? isp?.isp ?? asn?.autonomous_system_organization)
]);
if (result[0].changedRows ?? 0 > 0) {
++geoNamesInserted;
}
}
}
++progress;
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
if (elapsedSeconds > 10) {
logger.info(`Updating node location data ${progress}/${nodes.length}`);
loggerTimer = new Date().getTime() / 1000;
}
}
}
}
if (nodesUpdated > 0) {
logger.info(`${nodesUpdated} nodes maxmind data updated, ${geoNamesInserted} geo names inserted`);
} else {
logger.debug(`${nodesUpdated} nodes maxmind data updated, ${geoNamesInserted} geo names inserted`);
}
} catch (e) {
logger.err('$lookupNodeLocation() error: ' + (e instanceof Error ? e.message : e));
}
}

View File

@@ -0,0 +1,542 @@
import DB from '../../../database';
import { promises } from 'fs';
import logger from '../../../logger';
import fundingTxFetcher from './funding-tx-fetcher';
import config from '../../../config';
import { ILightningApi } from '../../../api/lightning/lightning-api.interface';
import { isIP } from 'net';
import { Common } from '../../../api/common';
import channelsApi from '../../../api/explorer/channels.api';
import nodesApi from '../../../api/explorer/nodes.api';
import { ResultSetHeader } from 'mysql2';
const fsPromises = promises;
class LightningStatsImporter {
topologiesFolder = config.LIGHTNING.TOPOLOGY_FOLDER;
async $run(): Promise<void> {
const [channels]: any[] = await DB.query('SELECT short_id from channels;');
logger.info('Caching funding txs for currently existing channels');
await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id));
if (config.MEMPOOL.NETWORK !== 'mainnet' || config.DATABASE.ENABLED === false) {
return;
}
await this.$importHistoricalLightningStats();
await this.$cleanupIncorrectSnapshot();
}
/**
* Generate LN network stats for one day
*/
public async computeNetworkStats(timestamp: number,
networkGraph: ILightningApi.NetworkGraph, isHistorical: boolean = false): Promise<unknown> {
// Node counts and network shares
let clearnetNodes = 0;
let torNodes = 0;
let clearnetTorNodes = 0;
let unannouncedNodes = 0;
const [nodesInDbRaw]: any[] = await DB.query(`SELECT public_key FROM nodes`);
const nodesInDb = {};
for (const node of nodesInDbRaw) {
nodesInDb[node.public_key] = node;
}
for (const node of networkGraph.nodes) {
// If we don't know about this node, insert it in db
if (isHistorical === true && !nodesInDb[node.pub_key]) {
await nodesApi.$saveNode({
last_update: node.last_update,
pub_key: node.pub_key,
alias: node.alias,
addresses: node.addresses,
color: node.color,
features: node.features,
});
nodesInDb[node.pub_key] = node;
} else {
await nodesApi.$updateNodeSockets(node.pub_key, node.addresses);
}
let hasOnion = false;
let hasClearnet = false;
let isUnnanounced = true;
for (const socket of (node.addresses ?? [])) {
if (!socket.network?.length && !socket.addr?.length) {
continue;
}
hasOnion = hasOnion || ['torv2', 'torv3'].includes(socket.network) || socket.addr.indexOf('onion') !== -1 || socket.addr.indexOf('torv2') !== -1 || socket.addr.indexOf('torv3') !== -1;
hasClearnet = hasClearnet || ['ipv4', 'ipv6'].includes(socket.network) || [4, 6].includes(isIP(socket.addr.split(':')[0])) || socket.addr.indexOf('ipv4') !== -1 || socket.addr.indexOf('ipv6') !== -1;;
}
if (hasOnion && hasClearnet) {
clearnetTorNodes++;
isUnnanounced = false;
} else if (hasOnion) {
torNodes++;
isUnnanounced = false;
} else if (hasClearnet) {
clearnetNodes++;
isUnnanounced = false;
}
if (isUnnanounced) {
unannouncedNodes++;
}
}
// Channels and node historical stats
const nodeStats = {};
let capacity = 0;
let avgFeeRate = 0;
let avgBaseFee = 0;
const capacities: number[] = [];
const feeRates: number[] = [];
const baseFees: number[] = [];
const alreadyCountedChannels = {};
const [channelsInDbRaw]: any[] = await DB.query(`SELECT short_id FROM channels`);
const channelsInDb = {};
for (const channel of channelsInDbRaw) {
channelsInDb[channel.short_id] = channel;
}
for (const channel of networkGraph.edges) {
const short_id = Common.channelIntegerIdToShortId(channel.channel_id);
const tx = await fundingTxFetcher.$fetchChannelOpenTx(short_id);
if (!tx) {
logger.err(`Unable to fetch funding tx for channel ${short_id}. Capacity and creation date is unknown. Skipping channel.`);
continue;
}
// If we don't know about this channel, insert it in db
if (isHistorical === true && !channelsInDb[short_id]) {
await channelsApi.$saveChannel({
channel_id: short_id,
chan_point: `${tx.txid}:${short_id.split('x')[2]}`,
last_update: channel.last_update,
node1_pub: channel.node1_pub,
node2_pub: channel.node2_pub,
capacity: (tx.value * 100000000).toString(),
node1_policy: null,
node2_policy: null,
}, 0);
channelsInDb[channel.channel_id] = channel;
}
if (!nodeStats[channel.node1_pub]) {
nodeStats[channel.node1_pub] = {
capacity: 0,
channels: 0,
};
}
if (!nodeStats[channel.node2_pub]) {
nodeStats[channel.node2_pub] = {
capacity: 0,
channels: 0,
};
}
if (!alreadyCountedChannels[short_id]) {
capacity += Math.round(tx.value * 100000000);
capacities.push(Math.round(tx.value * 100000000));
alreadyCountedChannels[short_id] = true;
nodeStats[channel.node1_pub].capacity += Math.round(tx.value * 100000000);
nodeStats[channel.node1_pub].channels++;
nodeStats[channel.node2_pub].capacity += Math.round(tx.value * 100000000);
nodeStats[channel.node2_pub].channels++;
}
if (isHistorical === false) { // Coming from the node
for (const policy of [channel.node1_policy, channel.node2_policy]) {
if (policy && parseInt(policy.fee_rate_milli_msat, 10) < 5000) {
avgFeeRate += parseInt(policy.fee_rate_milli_msat, 10);
feeRates.push(parseInt(policy.fee_rate_milli_msat, 10));
}
if (policy && parseInt(policy.fee_base_msat, 10) < 5000) {
avgBaseFee += parseInt(policy.fee_base_msat, 10);
baseFees.push(parseInt(policy.fee_base_msat, 10));
}
}
} else {
// @ts-ignore
if (channel.node1_policy.fee_rate_milli_msat < 5000) {
// @ts-ignore
avgFeeRate += parseInt(channel.node1_policy.fee_rate_milli_msat, 10);
// @ts-ignore
feeRates.push(parseInt(channel.node1_policy.fee_rate_milli_msat), 10);
}
// @ts-ignore
if (channel.node1_policy.fee_base_msat < 5000) {
// @ts-ignore
avgBaseFee += parseInt(channel.node1_policy.fee_base_msat, 10);
// @ts-ignore
baseFees.push(parseInt(channel.node1_policy.fee_base_msat), 10);
}
}
}
let medCapacity = 0;
let medFeeRate = 0;
let medBaseFee = 0;
let avgCapacity = 0;
avgFeeRate /= Math.max(networkGraph.edges.length, 1);
avgBaseFee /= Math.max(networkGraph.edges.length, 1);
if (capacities.length > 0) {
medCapacity = capacities.sort((a, b) => b - a)[Math.round(capacities.length / 2 - 1)];
avgCapacity = Math.round(capacity / Math.max(capacities.length, 1));
}
if (feeRates.length > 0) {
medFeeRate = feeRates.sort((a, b) => b - a)[Math.round(feeRates.length / 2 - 1)];
}
if (baseFees.length > 0) {
medBaseFee = baseFees.sort((a, b) => b - a)[Math.round(baseFees.length / 2 - 1)];
}
let query = `INSERT INTO lightning_stats(
added,
channel_count,
node_count,
total_capacity,
tor_nodes,
clearnet_nodes,
unannounced_nodes,
clearnet_tor_nodes,
avg_capacity,
avg_fee_rate,
avg_base_fee_mtokens,
med_capacity,
med_fee_rate,
med_base_fee_mtokens
)
VALUES (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
added = FROM_UNIXTIME(?),
channel_count = ?,
node_count = ?,
total_capacity = ?,
tor_nodes = ?,
clearnet_nodes = ?,
unannounced_nodes = ?,
clearnet_tor_nodes = ?,
avg_capacity = ?,
avg_fee_rate = ?,
avg_base_fee_mtokens = ?,
med_capacity = ?,
med_fee_rate = ?,
med_base_fee_mtokens = ?
`;
await DB.query(query, [
timestamp,
capacities.length,
networkGraph.nodes.length,
capacity,
torNodes,
clearnetNodes,
unannouncedNodes,
clearnetTorNodes,
avgCapacity,
avgFeeRate,
avgBaseFee,
medCapacity,
medFeeRate,
medBaseFee,
timestamp,
capacities.length,
networkGraph.nodes.length,
capacity,
torNodes,
clearnetNodes,
unannouncedNodes,
clearnetTorNodes,
avgCapacity,
avgFeeRate,
avgBaseFee,
medCapacity,
medFeeRate,
medBaseFee,
]);
for (const public_key of Object.keys(nodeStats)) {
query = `INSERT INTO node_stats(
public_key,
added,
capacity,
channels
)
VALUES (?, FROM_UNIXTIME(?), ?, ?)
ON DUPLICATE KEY UPDATE
added = FROM_UNIXTIME(?),
capacity = ?,
channels = ?
`;
await DB.query(query, [
public_key,
timestamp,
nodeStats[public_key].capacity,
nodeStats[public_key].channels,
timestamp,
nodeStats[public_key].capacity,
nodeStats[public_key].channels,
]);
if (!isHistorical) {
await DB.query(
`UPDATE nodes SET capacity = ?, channels = ? WHERE public_key = ?`,
[
nodeStats[public_key].capacity,
nodeStats[public_key].channels,
public_key,
]
);
}
}
return {
added: timestamp,
node_count: networkGraph.nodes.length
};
}
/**
* Import topology files LN historical data into the database
*/
async $importHistoricalLightningStats(): Promise<void> {
logger.debug('Run the historical importer');
try {
let fileList: string[] = [];
try {
fileList = await fsPromises.readdir(this.topologiesFolder);
} catch (e) {
logger.err(`Unable to open topology folder at ${this.topologiesFolder}`);
throw e;
}
// Insert history from the most recent to the oldest
// This also put the .json cached files first
fileList.sort().reverse();
const [rows]: any[] = await DB.query(`
SELECT UNIX_TIMESTAMP(added) AS added
FROM lightning_stats
ORDER BY added DESC
`);
const existingStatsTimestamps = {};
for (const row of rows) {
existingStatsTimestamps[row.added] = row;
}
// For logging purpose
let processed = 10;
let totalProcessed = 0;
let logStarted = false;
for (const filename of fileList) {
processed++;
const timestamp = parseInt(filename.split('_')[1], 10);
// Stats exist already, don't calculate/insert them
if (existingStatsTimestamps[timestamp] !== undefined) {
totalProcessed++;
continue;
}
if (filename.indexOf('topology_') === -1) {
totalProcessed++;
continue;
}
logger.debug(`Reading ${this.topologiesFolder}/${filename}`);
let fileContent = '';
try {
fileContent = await fsPromises.readFile(`${this.topologiesFolder}/${filename}`, 'utf8');
} catch (e: any) {
if (e.errno == -1) { // EISDIR - Ignore directorie
totalProcessed++;
continue;
}
logger.err(`Unable to open ${this.topologiesFolder}/${filename}`);
totalProcessed++;
continue;
}
let graph;
try {
graph = JSON.parse(fileContent);
graph = await this.cleanupTopology(graph);
} catch (e) {
logger.debug(`Invalid topology file ${this.topologiesFolder}/${filename}, cannot parse the content. Reason: ${e instanceof Error ? e.message : e}`);
totalProcessed++;
continue;
}
if (this.isIncorrectSnapshot(timestamp, graph)) {
logger.debug(`Ignoring ${this.topologiesFolder}/${filename}, because we defined it as an incorrect snapshot`);
++totalProcessed;
continue;
}
if (!logStarted) {
logger.info(`Founds a topology file that we did not import. Importing historical lightning stats now.`);
logStarted = true;
}
const datestr = `${new Date(timestamp * 1000).toUTCString()} (${timestamp})`;
logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.edges.length} channels`);
totalProcessed++;
if (processed > 10) {
logger.info(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`);
processed = 0;
} else {
logger.debug(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`);
}
await fundingTxFetcher.$fetchChannelsFundingTxs(graph.edges.map(channel => channel.channel_id.slice(0, -2)));
const stat = await this.computeNetworkStats(timestamp, graph, true);
existingStatsTimestamps[timestamp] = stat;
}
if (totalProcessed > 0) {
logger.info(`Lightning network stats historical import completed`);
}
} catch (e) {
logger.err(`Lightning network stats historical failed. Reason: ${e instanceof Error ? e.message : e}`);
}
}
cleanupTopology(graph): ILightningApi.NetworkGraph {
const newGraph = {
nodes: <ILightningApi.Node[]>[],
edges: <ILightningApi.Channel[]>[],
};
for (const node of graph.nodes) {
const addressesParts = (node.addresses ?? '').split(',');
const addresses: any[] = [];
for (const address of addressesParts) {
const formatted = Common.findSocketNetwork(address);
addresses.push({
network: formatted.network,
addr: formatted.url
});
}
let rgb = node.rgb_color ?? '#000000';
if (rgb.indexOf('#') === -1) {
rgb = `#${rgb}`;
}
newGraph.nodes.push({
last_update: node.timestamp ?? 0,
pub_key: node.id ?? null,
alias: node.alias ?? node.id.slice(0, 20),
addresses: addresses,
color: rgb,
features: {},
});
}
for (const adjacency of graph.adjacency) {
if (adjacency.length === 0) {
continue;
} else {
for (const edge of adjacency) {
newGraph.edges.push({
channel_id: edge.scid,
chan_point: '',
last_update: edge.timestamp,
node1_pub: edge.source ?? null,
node2_pub: edge.destination ?? null,
capacity: '0', // Will be fetch later
node1_policy: {
time_lock_delta: edge.cltv_expiry_delta,
min_htlc: edge.htlc_minimim_msat,
fee_base_msat: edge.fee_base_msat,
fee_rate_milli_msat: edge.fee_proportional_millionths,
max_htlc_msat: edge.htlc_maximum_msat,
last_update: edge.timestamp,
disabled: false,
},
node2_policy: null,
});
}
}
}
return newGraph;
}
private isIncorrectSnapshot(timestamp, graph): boolean {
if (timestamp >= 1549065600 /* 2019-02-02 */ && timestamp <= 1550620800 /* 2019-02-20 */ && graph.nodes.length < 2600) {
return true;
}
if (timestamp >= 1552953600 /* 2019-03-19 */ && timestamp <= 1556323200 /* 2019-05-27 */ && graph.nodes.length < 4000) {
return true;
}
if (timestamp >= 1557446400 /* 2019-05-10 */ && timestamp <= 1560470400 /* 2019-06-14 */ && graph.nodes.length < 4000) {
return true;
}
if (timestamp >= 1561680000 /* 2019-06-28 */ && timestamp <= 1563148800 /* 2019-07-15 */ && graph.nodes.length < 4000) {
return true;
}
if (timestamp >= 1571270400 /* 2019-11-17 */ && timestamp <= 1580601600 /* 2020-02-02 */ && graph.nodes.length < 4500) {
return true;
}
if (timestamp >= 1591142400 /* 2020-06-03 */ && timestamp <= 1592006400 /* 2020-06-13 */ && graph.nodes.length < 5500) {
return true;
}
if (timestamp >= 1632787200 /* 2021-09-28 */ && timestamp <= 1633564800 /* 2021-10-07 */ && graph.nodes.length < 13000) {
return true;
}
if (timestamp >= 1634256000 /* 2021-10-15 */ && timestamp <= 1645401600 /* 2022-02-21 */ && graph.nodes.length < 17000) {
return true;
}
if (timestamp >= 1654992000 /* 2022-06-12 */ && timestamp <= 1661472000 /* 2022-08-26 */ && graph.nodes.length < 14000) {
return true;
}
return false;
}
private async $cleanupIncorrectSnapshot(): Promise<void> {
// We do not run this one automatically because those stats are not supposed to be inserted in the first
// place, but I write them here to remind us we manually run those queries
// DELETE FROM lightning_stats
// WHERE (
// UNIX_TIMESTAMP(added) >= 1549065600 AND UNIX_TIMESTAMP(added) <= 1550620800 AND node_count < 2600 OR
// UNIX_TIMESTAMP(added) >= 1552953600 AND UNIX_TIMESTAMP(added) <= 1556323200 AND node_count < 4000 OR
// UNIX_TIMESTAMP(added) >= 1557446400 AND UNIX_TIMESTAMP(added) <= 1560470400 AND node_count < 4000 OR
// UNIX_TIMESTAMP(added) >= 1561680000 AND UNIX_TIMESTAMP(added) <= 1563148800 AND node_count < 4000 OR
// UNIX_TIMESTAMP(added) >= 1571270400 AND UNIX_TIMESTAMP(added) <= 1580601600 AND node_count < 4500 OR
// UNIX_TIMESTAMP(added) >= 1591142400 AND UNIX_TIMESTAMP(added) <= 1592006400 AND node_count < 5500 OR
// UNIX_TIMESTAMP(added) >= 1632787200 AND UNIX_TIMESTAMP(added) <= 1633564800 AND node_count < 13000 OR
// UNIX_TIMESTAMP(added) >= 1634256000 AND UNIX_TIMESTAMP(added) <= 1645401600 AND node_count < 17000 OR
// UNIX_TIMESTAMP(added) >= 1654992000 AND UNIX_TIMESTAMP(added) <= 1661472000 AND node_count < 14000
// )
// DELETE FROM node_stats
// WHERE (
// UNIX_TIMESTAMP(added) >= 1549065600 AND UNIX_TIMESTAMP(added) <= 1550620800 OR
// UNIX_TIMESTAMP(added) >= 1552953600 AND UNIX_TIMESTAMP(added) <= 1556323200 OR
// UNIX_TIMESTAMP(added) >= 1557446400 AND UNIX_TIMESTAMP(added) <= 1560470400 OR
// UNIX_TIMESTAMP(added) >= 1561680000 AND UNIX_TIMESTAMP(added) <= 1563148800 OR
// UNIX_TIMESTAMP(added) >= 1571270400 AND UNIX_TIMESTAMP(added) <= 1580601600 OR
// UNIX_TIMESTAMP(added) >= 1591142400 AND UNIX_TIMESTAMP(added) <= 1592006400 OR
// UNIX_TIMESTAMP(added) >= 1632787200 AND UNIX_TIMESTAMP(added) <= 1633564800 OR
// UNIX_TIMESTAMP(added) >= 1634256000 AND UNIX_TIMESTAMP(added) <= 1645401600 OR
// UNIX_TIMESTAMP(added) >= 1654992000 AND UNIX_TIMESTAMP(added) <= 1661472000
// )
}
}
export default new LightningStatsImporter;

View File

@@ -12,12 +12,11 @@ import * as https from 'https';
*/
class PoolsUpdater {
lastRun: number = 0;
currentSha: any = undefined;
currentSha: string | undefined = undefined;
poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL;
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL;
constructor() {
}
public async updatePoolsJson() {
public async updatePoolsJson(): Promise<void> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
return;
}
@@ -32,11 +31,10 @@ class PoolsUpdater {
this.lastRun = now;
logger.info('Updating latest mining pools from Github');
if (config.SOCKS5PROXY.ENABLED) {
logger.info('List of public pools will be queried over the Tor network');
logger.info(`Updating latest mining pools from ${this.poolsUrl} over the Tor network`);
} else {
logger.info('List of public pools will be queried over clearnet');
logger.info(`Updating latest mining pools from ${this.poolsUrl} over clearnet`);
}
try {
@@ -46,7 +44,7 @@ class PoolsUpdater {
}
if (config.DATABASE.ENABLED === true) {
this.currentSha = await this.getShaFromDb();
this.currentSha = await this.getShaFromDb();
}
logger.debug(`Pools.json sha | Current: ${this.currentSha} | Github: ${githubSha}`);
@@ -54,8 +52,12 @@ class PoolsUpdater {
return;
}
logger.warn('Pools.json is outdated, fetch latest from github');
const poolsJson = await this.query('https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json');
if (this.currentSha === undefined) {
logger.info(`Downloading pools.json for the first time from ${this.poolsUrl}`);
} else {
logger.warn(`Pools.json is outdated, fetch latest from ${this.poolsUrl}`);
}
const poolsJson = await this.query(this.poolsUrl);
if (poolsJson === undefined) {
return;
}
@@ -65,21 +67,21 @@ class PoolsUpdater {
} catch (e) {
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
logger.err('PoolsUpdater failed. Will try again in 24h. Reason: ' + (e instanceof Error ? e.message : e));
logger.err('PoolsUpdater failed. Will try again in 24h. Reason: ' + (e instanceof Error ? e.message : e));
}
}
/**
* Fetch our latest pools.json sha from the db
*/
private async updateDBSha(githubSha: string) {
private async updateDBSha(githubSha: string): Promise<void> {
this.currentSha = githubSha;
if (config.DATABASE.ENABLED === true) {
try {
await DB.query('DELETE FROM state where name="pools_json_sha"');
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
} catch (e) {
logger.err('Cannot save github pools.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e));
logger.err('Cannot save github pools.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e));
}
}
}
@@ -92,7 +94,7 @@ class PoolsUpdater {
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
return (rows.length > 0 ? rows[0].string : undefined);
} catch (e) {
logger.err('Cannot fetch pools.json sha from db. Reason: ' + (e instanceof Error ? e.message : e));
logger.err('Cannot fetch pools.json sha from db. Reason: ' + (e instanceof Error ? e.message : e));
return undefined;
}
}
@@ -101,7 +103,7 @@ class PoolsUpdater {
* Fetch our latest pools.json sha from github
*/
private async fetchPoolsSha(): Promise<string | undefined> {
const response = await this.query('https://api.github.com/repos/mempool/mining-pools/git/trees/master');
const response = await this.query(this.treeUrl);
if (response !== undefined) {
for (const file of response['tree']) {
@@ -111,7 +113,7 @@ class PoolsUpdater {
}
}
logger.err('Cannot to find latest pools.json sha from github api response');
logger.err(`Cannot find "pools.json" in git tree (${this.treeUrl})`);
return undefined;
}
@@ -125,7 +127,7 @@ class PoolsUpdater {
};
timeout: number;
httpsAgent?: https.Agent;
}
};
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
const axiosOptions: axiosOptions = {
headers: {
@@ -135,7 +137,7 @@ class PoolsUpdater {
};
let retry = 0;
while(retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
while (retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
try {
if (config.SOCKS5PROXY.ENABLED) {
const socksOptions: any = {
@@ -156,14 +158,14 @@ class PoolsUpdater {
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
}
const data: AxiosResponse = await axios.get(path, axiosOptions);
if (data.statusText === 'error' || !data.data) {
throw new Error(`Could not fetch data from Github, Error: ${data.status}`);
throw new Error(`Could not fetch data from ${path}, Error: ${data.status}`);
}
return data.data;
} catch (e) {
logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e));
logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e));
retry++;
}
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);

View File

@@ -0,0 +1,43 @@
import { query } from '../../utils/axios-query';
import priceUpdater, { PriceFeed, PriceHistory } from '../price-updater';
class BitfinexApi implements PriceFeed {
public name: string = 'Bitfinex';
public currencies: string[] = ['USD', 'EUR', 'GPB', 'JPY'];
public url: string = 'https://api.bitfinex.com/v1/pubticker/BTC';
public urlHist: string = 'https://api-pub.bitfinex.com/v2/candles/trade:{GRANULARITY}:tBTC{CURRENCY}/hist';
constructor() {
}
public async $fetchPrice(currency): Promise<number> {
const response = await query(this.url + currency);
return response ? parseInt(response['last_price'], 10) : -1;
}
public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise<PriceHistory> {
const priceHistory: PriceHistory = {};
for (const currency of currencies) {
if (this.currencies.includes(currency) === false) {
continue;
}
const response = await query(this.urlHist.replace('{GRANULARITY}', type === 'hour' ? '1h' : '1D').replace('{CURRENCY}', currency));
const pricesRaw = response ? response : [];
for (const price of pricesRaw as any[]) {
const time = Math.round(price[0] / 1000);
if (priceHistory[time] === undefined) {
priceHistory[time] = priceUpdater.getEmptyPricesObj();
}
priceHistory[time][currency] = price[2];
}
}
return priceHistory;
}
}
export default BitfinexApi;

View File

@@ -0,0 +1,24 @@
import { query } from '../../utils/axios-query';
import { PriceFeed, PriceHistory } from '../price-updater';
class BitflyerApi implements PriceFeed {
public name: string = 'Bitflyer';
public currencies: string[] = ['USD', 'EUR', 'JPY'];
public url: string = 'https://api.bitflyer.com/v1/ticker?product_code=BTC_';
public urlHist: string = '';
constructor() {
}
public async $fetchPrice(currency): Promise<number> {
const response = await query(this.url + currency);
return response ? parseInt(response['ltp'], 10) : -1;
}
public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise<PriceHistory> {
return [];
}
}
export default BitflyerApi;

View File

@@ -0,0 +1,42 @@
import { query } from '../../utils/axios-query';
import priceUpdater, { PriceFeed, PriceHistory } from '../price-updater';
class CoinbaseApi implements PriceFeed {
public name: string = 'Coinbase';
public currencies: string[] = ['USD', 'EUR', 'GBP'];
public url: string = 'https://api.coinbase.com/v2/prices/spot?currency=';
public urlHist: string = 'https://api.exchange.coinbase.com/products/BTC-{CURRENCY}/candles?granularity={GRANULARITY}';
constructor() {
}
public async $fetchPrice(currency): Promise<number> {
const response = await query(this.url + currency);
return response ? parseInt(response['data']['amount'], 10) : -1;
}
public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise<PriceHistory> {
const priceHistory: PriceHistory = {};
for (const currency of currencies) {
if (this.currencies.includes(currency) === false) {
continue;
}
const response = await query(this.urlHist.replace('{GRANULARITY}', type === 'hour' ? '3600' : '86400').replace('{CURRENCY}', currency));
const pricesRaw = response ? response : [];
for (const price of pricesRaw as any[]) {
if (priceHistory[price[0]] === undefined) {
priceHistory[price[0]] = priceUpdater.getEmptyPricesObj();
}
priceHistory[price[0]][currency] = price[4];
}
}
return priceHistory;
}
}
export default CoinbaseApi;

View File

@@ -0,0 +1,43 @@
import { query } from '../../utils/axios-query';
import priceUpdater, { PriceFeed, PriceHistory } from '../price-updater';
class FtxApi implements PriceFeed {
public name: string = 'FTX';
public currencies: string[] = ['USD', 'BRZ', 'EUR', 'JPY', 'AUD'];
public url: string = 'https://ftx.com/api/markets/BTC/';
public urlHist: string = 'https://ftx.com/api/markets/BTC/{CURRENCY}/candles?resolution={GRANULARITY}';
constructor() {
}
public async $fetchPrice(currency): Promise<number> {
const response = await query(this.url + currency);
return response ? parseInt(response['result']['last'], 10) : -1;
}
public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise<PriceHistory> {
const priceHistory: PriceHistory = {};
for (const currency of currencies) {
if (this.currencies.includes(currency) === false) {
continue;
}
const response = await query(this.urlHist.replace('{GRANULARITY}', type === 'hour' ? '3600' : '86400').replace('{CURRENCY}', currency));
const pricesRaw = response ? response['result'] : [];
for (const price of pricesRaw as any[]) {
const time = Math.round(price['time'] / 1000);
if (priceHistory[time] === undefined) {
priceHistory[time] = priceUpdater.getEmptyPricesObj();
}
priceHistory[time][currency] = price['close'];
}
}
return priceHistory;
}
}
export default FtxApi;

View File

@@ -0,0 +1,43 @@
import { query } from '../../utils/axios-query';
import priceUpdater, { PriceFeed, PriceHistory } from '../price-updater';
class GeminiApi implements PriceFeed {
public name: string = 'Gemini';
public currencies: string[] = ['USD', 'EUR', 'GBP', 'SGD'];
public url: string = 'https://api.gemini.com/v1/pubticker/BTC';
public urlHist: string = 'https://api.gemini.com/v2/candles/BTC{CURRENCY}/{GRANULARITY}';
constructor() {
}
public async $fetchPrice(currency): Promise<number> {
const response = await query(this.url + currency);
return response ? parseInt(response['last'], 10) : -1;
}
public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise<PriceHistory> {
const priceHistory: PriceHistory = {};
for (const currency of currencies) {
if (this.currencies.includes(currency) === false) {
continue;
}
const response = await query(this.urlHist.replace('{GRANULARITY}', type === 'hour' ? '1hr' : '1day').replace('{CURRENCY}', currency));
const pricesRaw = response ? response : [];
for (const price of pricesRaw as any[]) {
const time = Math.round(price[0] / 1000);
if (priceHistory[time] === undefined) {
priceHistory[time] = priceUpdater.getEmptyPricesObj();
}
priceHistory[time][currency] = price[4];
}
}
return priceHistory;
}
}
export default GeminiApi;

View File

@@ -0,0 +1,99 @@
import logger from '../../logger';
import PricesRepository from '../../repositories/PricesRepository';
import { query } from '../../utils/axios-query';
import priceUpdater, { PriceFeed, PriceHistory } from '../price-updater';
class KrakenApi implements PriceFeed {
public name: string = 'Kraken';
public currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'];
public url: string = 'https://api.kraken.com/0/public/Ticker?pair=XBT';
public urlHist: string = 'https://api.kraken.com/0/public/OHLC?interval={GRANULARITY}&pair=XBT';
constructor() {
}
private getTicker(currency) {
let ticker = `XXBTZ${currency}`;
if (['CHF', 'AUD'].includes(currency)) {
ticker = `XBT${currency}`;
}
return ticker;
}
public async $fetchPrice(currency): Promise<number> {
const response = await query(this.url + currency);
return response ? parseInt(response['result'][this.getTicker(currency)]['c'][0], 10) : -1;
}
public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise<PriceHistory> {
const priceHistory: PriceHistory = {};
for (const currency of currencies) {
if (this.currencies.includes(currency) === false) {
continue;
}
const response = await query(this.urlHist.replace('{GRANULARITY}', '60') + currency);
const pricesRaw = response ? response['result'][this.getTicker(currency)] : [];
for (const price of pricesRaw) {
if (priceHistory[price[0]] === undefined) {
priceHistory[price[0]] = priceUpdater.getEmptyPricesObj();
}
priceHistory[price[0]][currency] = price[4];
}
}
return priceHistory;
}
/**
* Fetch weekly price and save it into the database
*/
public async $insertHistoricalPrice(): Promise<void> {
const existingPriceTimes = await PricesRepository.$getPricesTimes();
// EUR weekly price history goes back to timestamp 1378339200 (September 5, 2013)
// USD weekly price history goes back to timestamp 1380758400 (October 3, 2013)
// GBP weekly price history goes back to timestamp 1415232000 (November 6, 2014)
// JPY weekly price history goes back to timestamp 1415232000 (November 6, 2014)
// CAD weekly price history goes back to timestamp 1436400000 (July 9, 2015)
// CHF weekly price history goes back to timestamp 1575504000 (December 5, 2019)
// AUD weekly price history goes back to timestamp 1591833600 (June 11, 2020)
let priceHistory: any = {}; // map: timestamp -> Prices
for (const currency of this.currencies) {
const response = await query(this.urlHist.replace('{GRANULARITY}', '10080') + currency);
const priceHistoryRaw = response ? response['result'][this.getTicker(currency)] : [];
for (const price of priceHistoryRaw) {
if (existingPriceTimes.includes(parseInt(price[0]))) {
continue;
}
// prices[0] = kraken price timestamp
// prices[4] = closing price
if (priceHistory[price[0]] === undefined) {
priceHistory[price[0]] = priceUpdater.getEmptyPricesObj();
}
priceHistory[price[0]][currency] = price[4];
}
}
for (const time in priceHistory) {
if (priceHistory[time].USD === -1) {
delete priceHistory[time];
continue;
}
await PricesRepository.$savePrices(parseInt(time, 10), priceHistory[time]);
}
if (Object.keys(priceHistory).length > 0) {
logger.notice(`Inserted ${Object.keys(priceHistory).length} Kraken EUR, USD, GBP, JPY, CAD, CHF and AUD weekly price history into db`);
}
}
}
export default KrakenApi;

View File

@@ -0,0 +1,762 @@
[
{
"ct": 1279497600,
"c": "0.08584"
},
{
"ct": 1280102400,
"c": "0.0505"
},
{
"ct": 1280707200,
"c": "0.0611"
},
{
"ct": 1281312000,
"c": "0.0609"
},
{
"ct": 1281916800,
"c": "0.06529"
},
{
"ct": 1282521600,
"c": "0.066"
},
{
"ct": 1283126400,
"c": "0.064"
},
{
"ct": 1283731200,
"c": "0.06165"
},
{
"ct": 1284336000,
"c": "0.0615"
},
{
"ct": 1284940800,
"c": "0.0627"
},
{
"ct": 1285545600,
"c": "0.0622"
},
{
"ct": 1286150400,
"c": "0.06111"
},
{
"ct": 1286755200,
"c": "0.0965"
},
{
"ct": 1287360000,
"c": "0.102"
},
{
"ct": 1287964800,
"c": "0.11501"
},
{
"ct": 1288569600,
"c": "0.1925"
},
{
"ct": 1289174400,
"c": "0.34"
},
{
"ct": 1289779200,
"c": "0.27904"
},
{
"ct": 1290384000,
"c": "0.27675"
},
{
"ct": 1290988800,
"c": "0.27"
},
{
"ct": 1291593600,
"c": "0.19"
},
{
"ct": 1292198400,
"c": "0.2189"
},
{
"ct": 1292803200,
"c": "0.2401"
},
{
"ct": 1293408000,
"c": "0.263"
},
{
"ct": 1294012800,
"c": "0.29997"
},
{
"ct": 1294617600,
"c": "0.323"
},
{
"ct": 1295222400,
"c": "0.38679"
},
{
"ct": 1295827200,
"c": "0.4424"
},
{
"ct": 1296432000,
"c": "0.4799"
},
{
"ct": 1297036800,
"c": "0.8968"
},
{
"ct": 1297641600,
"c": "1.05"
},
{
"ct": 1298246400,
"c": "0.865"
},
{
"ct": 1298851200,
"c": "0.89"
},
{
"ct": 1299456000,
"c": "0.8999"
},
{
"ct": 1300060800,
"c": "0.89249"
},
{
"ct": 1300665600,
"c": "0.75218"
},
{
"ct": 1301270400,
"c": "0.82754"
},
{
"ct": 1301875200,
"c": "0.779"
},
{
"ct": 1302480000,
"c": "0.7369"
},
{
"ct": 1303084800,
"c": "1.1123"
},
{
"ct": 1303689600,
"c": "1.6311"
},
{
"ct": 1304294400,
"c": "3.03311"
},
{
"ct": 1304899200,
"c": "3.8659"
},
{
"ct": 1305504000,
"c": "6.98701"
},
{
"ct": 1306108800,
"c": "6.6901"
},
{
"ct": 1306713600,
"c": "8.4"
},
{
"ct": 1307318400,
"c": "16.7"
},
{
"ct": 1307923200,
"c": "18.5464"
},
{
"ct": 1308528000,
"c": "17.51"
},
{
"ct": 1309132800,
"c": "16.45001"
},
{
"ct": 1309737600,
"c": "15.44049"
},
{
"ct": 1310342400,
"c": "14.879"
},
{
"ct": 1310947200,
"c": "13.16"
},
{
"ct": 1311552000,
"c": "13.98001"
},
{
"ct": 1312156800,
"c": "13.35"
},
{
"ct": 1312761600,
"c": "7.9"
},
{
"ct": 1313366400,
"c": "10.7957"
},
{
"ct": 1313971200,
"c": "11.31125"
},
{
"ct": 1314576000,
"c": "9.07011"
},
{
"ct": 1315180800,
"c": "8.17798"
},
{
"ct": 1315785600,
"c": "5.86436"
},
{
"ct": 1316390400,
"c": "5.2"
},
{
"ct": 1316995200,
"c": "5.33"
},
{
"ct": 1317600000,
"c": "5.02701"
},
{
"ct": 1318204800,
"c": "4.10288"
},
{
"ct": 1318809600,
"c": "3.5574"
},
{
"ct": 1319414400,
"c": "3.12657"
},
{
"ct": 1320019200,
"c": "3.27"
},
{
"ct": 1320624000,
"c": "2.95959"
},
{
"ct": 1321228800,
"c": "2.99626"
},
{
"ct": 1321833600,
"c": "2.2"
},
{
"ct": 1322438400,
"c": "2.47991"
},
{
"ct": 1323043200,
"c": "2.82809"
},
{
"ct": 1323648000,
"c": "3.2511"
},
{
"ct": 1324252800,
"c": "3.193"
},
{
"ct": 1324857600,
"c": "4.225"
},
{
"ct": 1325462400,
"c": "5.26766"
},
{
"ct": 1326067200,
"c": "7.11358"
},
{
"ct": 1326672000,
"c": "7.00177"
},
{
"ct": 1327276800,
"c": "6.3097"
},
{
"ct": 1327881600,
"c": "5.38191"
},
{
"ct": 1328486400,
"c": "5.68881"
},
{
"ct": 1329091200,
"c": "5.51468"
},
{
"ct": 1329696000,
"c": "4.38669"
},
{
"ct": 1330300800,
"c": "4.922"
},
{
"ct": 1330905600,
"c": "4.8201"
},
{
"ct": 1331510400,
"c": "4.90901"
},
{
"ct": 1332115200,
"c": "5.27943"
},
{
"ct": 1332720000,
"c": "4.55001"
},
{
"ct": 1333324800,
"c": "4.81922"
},
{
"ct": 1333929600,
"c": "4.79253"
},
{
"ct": 1334534400,
"c": "4.96892"
},
{
"ct": 1335139200,
"c": "5.20352"
},
{
"ct": 1335744000,
"c": "4.90441"
},
{
"ct": 1336348800,
"c": "5.04991"
},
{
"ct": 1336953600,
"c": "4.92996"
},
{
"ct": 1337558400,
"c": "5.09002"
},
{
"ct": 1338163200,
"c": "5.13896"
},
{
"ct": 1338768000,
"c": "5.2051"
},
{
"ct": 1339372800,
"c": "5.46829"
},
{
"ct": 1339977600,
"c": "6.16382"
},
{
"ct": 1340582400,
"c": "6.35002"
},
{
"ct": 1341187200,
"c": "6.62898"
},
{
"ct": 1341792000,
"c": "6.79898"
},
{
"ct": 1342396800,
"c": "7.62101"
},
{
"ct": 1343001600,
"c": "8.4096"
},
{
"ct": 1343606400,
"c": "8.71027"
},
{
"ct": 1344211200,
"c": "10.86998"
},
{
"ct": 1344816000,
"c": "11.6239"
},
{
"ct": 1345420800,
"c": "7.98"
},
{
"ct": 1346025600,
"c": "10.61"
},
{
"ct": 1346630400,
"c": "10.2041"
},
{
"ct": 1347235200,
"c": "11.02"
},
{
"ct": 1347840000,
"c": "11.87"
},
{
"ct": 1348444800,
"c": "12.19331"
},
{
"ct": 1349049600,
"c": "12.4"
},
{
"ct": 1349654400,
"c": "11.8034"
},
{
"ct": 1350259200,
"c": "11.7389"
},
{
"ct": 1350864000,
"c": "11.63107"
},
{
"ct": 1351468800,
"c": "10.69998"
},
{
"ct": 1352073600,
"c": "10.80011"
},
{
"ct": 1352678400,
"c": "10.84692"
},
{
"ct": 1353283200,
"c": "11.65961"
},
{
"ct": 1353888000,
"c": "12.4821"
},
{
"ct": 1354492800,
"c": "12.50003"
},
{
"ct": 1355097600,
"c": "13.388"
},
{
"ct": 1355702400,
"c": "13.30002"
},
{
"ct": 1356307200,
"c": "13.31202"
},
{
"ct": 1356912000,
"c": "13.45001"
},
{
"ct": 1357516800,
"c": "13.5199"
},
{
"ct": 1358121600,
"c": "14.11601"
},
{
"ct": 1358726400,
"c": "15.7"
},
{
"ct": 1359331200,
"c": "17.95"
},
{
"ct": 1359936000,
"c": "20.59"
},
{
"ct": 1360540800,
"c": "23.96975"
},
{
"ct": 1361145600,
"c": "26.8146"
},
{
"ct": 1361750400,
"c": "29.88999"
},
{
"ct": 1362355200,
"c": "34.49999"
},
{
"ct": 1362960000,
"c": "46"
},
{
"ct": 1363564800,
"c": "47.4"
},
{
"ct": 1364169600,
"c": "71.93"
},
{
"ct": 1364774400,
"c": "93.03001"
},
{
"ct": 1365379200,
"c": "162.30102"
},
{
"ct": 1365984000,
"c": "89.99999"
},
{
"ct": 1366588800,
"c": "119.2"
},
{
"ct": 1367193600,
"c": "134.44444"
},
{
"ct": 1367798400,
"c": "115.98"
},
{
"ct": 1368403200,
"c": "114.82002"
},
{
"ct": 1369008000,
"c": "122.49999"
},
{
"ct": 1369612800,
"c": "133.5"
},
{
"ct": 1370217600,
"c": "122.5"
},
{
"ct": 1370822400,
"c": "100.43743"
},
{
"ct": 1371427200,
"c": "99.9"
},
{
"ct": 1372032000,
"c": "107.90001"
},
{
"ct": 1372636800,
"c": "97.51"
},
{
"ct": 1373241600,
"c": "76.5"
},
{
"ct": 1373846400,
"c": "94.41986"
},
{
"ct": 1374451200,
"c": "91.998"
},
{
"ct": 1375056000,
"c": "98.78008"
},
{
"ct": 1375660800,
"c": "105.12"
},
{
"ct": 1376265600,
"c": "105"
},
{
"ct": 1376870400,
"c": "113.38"
},
{
"ct": 1377475200,
"c": "122.11102"
},
{
"ct": 1378080000,
"c": "146.01003"
},
{
"ct": 1378684800,
"c": "126.31501"
},
{
"ct": 1379289600,
"c": "138.3002"
},
{
"ct": 1379894400,
"c": "134.00001"
},
{
"ct": 1380499200,
"c": "143.88402"
},
{
"ct": 1381104000,
"c": "137.8"
},
{
"ct": 1381708800,
"c": "147.53"
},
{
"ct": 1382313600,
"c": "186.1"
},
{
"ct": 1382918400,
"c": "207.0001"
},
{
"ct": 1383523200,
"c": "224.01001"
},
{
"ct": 1384128000,
"c": "336.33101"
},
{
"ct": 1384732800,
"c": "528"
},
{
"ct": 1385337600,
"c": "795"
},
{
"ct": 1385942400,
"c": "1004.42392"
},
{
"ct": 1386547200,
"c": "804.5"
},
{
"ct": 1387152000,
"c": "919.985"
},
{
"ct": 1387756800,
"c": "639.48"
},
{
"ct": 1388361600,
"c": "786.98"
},
{
"ct": 1388966400,
"c": "1015"
},
{
"ct": 1389571200,
"c": "940"
},
{
"ct": 1390176000,
"c": "954.995"
},
{
"ct": 1390780800,
"c": "1007.98999"
},
{
"ct": 1391385600,
"c": "954"
},
{
"ct": 1391990400,
"c": "659.49776"
},
{
"ct": 1392595200,
"c": "299.702"
},
{
"ct": 1393200000,
"c": "310.00001"
},
{
"ct": 1393804800,
"c": "135"
}
]

View File

@@ -0,0 +1,265 @@
import * as fs from 'fs';
import path from "path";
import { Common } from '../api/common';
import config from '../config';
import logger from '../logger';
import PricesRepository from '../repositories/PricesRepository';
import BitfinexApi from './price-feeds/bitfinex-api';
import BitflyerApi from './price-feeds/bitflyer-api';
import CoinbaseApi from './price-feeds/coinbase-api';
import FtxApi from './price-feeds/ftx-api';
import GeminiApi from './price-feeds/gemini-api';
import KrakenApi from './price-feeds/kraken-api';
export interface PriceFeed {
name: string;
url: string;
urlHist: string;
currencies: string[];
$fetchPrice(currency): Promise<number>;
$fetchRecentPrice(currencies: string[], type: string): Promise<PriceHistory>;
}
export interface PriceHistory {
[timestamp: number]: Prices;
}
export interface Prices {
USD: number;
EUR: number;
GBP: number;
CAD: number;
CHF: number;
AUD: number;
JPY: number;
}
class PriceUpdater {
public historyInserted = false;
lastRun = 0;
lastHistoricalRun = 0;
running = false;
feeds: PriceFeed[] = [];
currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'];
latestPrices: Prices;
constructor() {
this.latestPrices = this.getEmptyPricesObj();
this.feeds.push(new BitflyerApi()); // Does not have historical endpoint
this.feeds.push(new FtxApi());
this.feeds.push(new KrakenApi());
this.feeds.push(new CoinbaseApi());
this.feeds.push(new BitfinexApi());
this.feeds.push(new GeminiApi());
}
public getEmptyPricesObj(): Prices {
return {
USD: -1,
EUR: -1,
GBP: -1,
CAD: -1,
CHF: -1,
AUD: -1,
JPY: -1,
};
}
public async $run(): Promise<void> {
if (this.running === true) {
return;
}
this.running = true;
if ((Math.round(new Date().getTime() / 1000) - this.lastHistoricalRun) > 3600 * 24) {
// Once a day, look for missing prices (could happen due to network connectivity issues)
this.historyInserted = false;
}
try {
if (this.historyInserted === false && config.DATABASE.ENABLED === true) {
await this.$insertHistoricalPrices();
} else {
await this.$updatePrice();
}
} catch (e) {
logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`);
}
this.running = false;
}
/**
* Fetch last BTC price from exchanges, average them, and save it in the database once every hour
*/
private async $updatePrice(): Promise<void> {
if (this.lastRun === 0 && config.DATABASE.ENABLED === true) {
this.lastRun = await PricesRepository.$getLatestPriceTime();
}
if ((Math.round(new Date().getTime() / 1000) - this.lastRun) < 3600) {
// Refresh only once every hour
return;
}
const previousRun = this.lastRun;
this.lastRun = new Date().getTime() / 1000;
for (const currency of this.currencies) {
let prices: number[] = [];
for (const feed of this.feeds) {
// Fetch prices from API which supports `currency`
if (feed.currencies.includes(currency)) {
try {
const price = await feed.$fetchPrice(currency);
if (price > 0) {
prices.push(price);
}
logger.debug(`${feed.name} BTC/${currency} price: ${price}`);
} catch (e) {
logger.debug(`Could not fetch BTC/${currency} price at ${feed.name}. Reason: ${(e instanceof Error ? e.message : e)}`);
}
}
}
if (prices.length === 1) {
logger.debug(`Only ${prices.length} feed available for BTC/${currency} price`);
}
// Compute average price, non weighted
prices = prices.filter(price => price > 0);
this.latestPrices[currency] = Math.round((prices.reduce((partialSum, a) => partialSum + a, 0)) / prices.length);
}
logger.info(`Latest BTC fiat averaged price: ${JSON.stringify(this.latestPrices)}`);
if (config.DATABASE.ENABLED === true) {
// Save everything in db
try {
const p = 60 * 60 * 1000; // milliseconds in an hour
const nowRounded = new Date(Math.round(new Date().getTime() / p) * p); // https://stackoverflow.com/a/28037042
await PricesRepository.$savePrices(nowRounded.getTime() / 1000, this.latestPrices);
} catch (e) {
this.lastRun = previousRun + 5 * 60;
logger.err(`Cannot save latest prices into db. Trying again in 5 minutes. Reason: ${(e instanceof Error ? e.message : e)}`);
}
}
this.lastRun = new Date().getTime() / 1000;
}
/**
* Called once by the database migration to initialize historical prices data (weekly)
* We use MtGox weekly price from July 19, 2010 to September 30, 2013
* We use Kraken weekly price from October 3, 2013 up to last month
* We use Kraken hourly price for the past month
*/
private async $insertHistoricalPrices(): Promise<void> {
const existingPriceTimes = await PricesRepository.$getPricesTimes();
// Insert MtGox weekly prices
const pricesJson: any[] = JSON.parse(fs.readFileSync(path.join(__dirname, 'mtgox-weekly.json')).toString());
const prices = this.getEmptyPricesObj();
let insertedCount: number = 0;
for (const price of pricesJson) {
if (existingPriceTimes.includes(price['ct'])) {
continue;
}
// From 1380758400 we will use Kraken price as it follows closely MtGox, but was not affected as much
// by the MtGox exchange collapse a few months later
if (price['ct'] > 1380758400) {
break;
}
prices.USD = price['c'];
await PricesRepository.$savePrices(price['ct'], prices);
++insertedCount;
}
if (insertedCount > 0) {
logger.notice(`Inserted ${insertedCount} MtGox USD weekly price history into db`);
} else {
logger.debug(`Inserted ${insertedCount} MtGox USD weekly price history into db`);
}
// Insert Kraken weekly prices
await new KrakenApi().$insertHistoricalPrice();
// Insert missing recent hourly prices
await this.$insertMissingRecentPrices('day');
await this.$insertMissingRecentPrices('hour');
this.historyInserted = true;
this.lastHistoricalRun = new Date().getTime();
}
/**
* Find missing hourly prices and insert them in the database
* It has a limited backward range and it depends on which API are available
*/
private async $insertMissingRecentPrices(type: 'hour' | 'day'): Promise<void> {
const existingPriceTimes = await PricesRepository.$getPricesTimes();
logger.info(`Fetching ${type === 'day' ? 'dai' : 'hour'}ly price history from exchanges and saving missing ones into the database, this may take a while`);
const historicalPrices: PriceHistory[] = [];
// Fetch all historical hourly prices
for (const feed of this.feeds) {
try {
historicalPrices.push(await feed.$fetchRecentPrice(this.currencies, type));
} catch (e) {
logger.err(`Cannot fetch hourly historical price from ${feed.name}. Ignoring this feed. Reason: ${e instanceof Error ? e.message : e}`);
}
}
// Group them by timestamp and currency, for example
// grouped[123456789]['USD'] = [1, 2, 3, 4];
const grouped: Object = {};
for (const historicalEntry of historicalPrices) {
for (const time in historicalEntry) {
if (existingPriceTimes.includes(parseInt(time, 10))) {
continue;
}
if (grouped[time] === undefined) {
grouped[time] = {
USD: [], EUR: [], GBP: [], CAD: [], CHF: [], AUD: [], JPY: []
};
}
for (const currency of this.currencies) {
const price = historicalEntry[time][currency];
if (price > 0) {
grouped[time][currency].push(parseInt(price, 10));
}
}
}
}
// Average prices and insert everything into the db
let totalInserted = 0;
for (const time in grouped) {
const prices: Prices = this.getEmptyPricesObj();
for (const currency in grouped[time]) {
if (grouped[time][currency].length === 0) {
continue;
}
prices[currency] = Math.round((grouped[time][currency].reduce(
(partialSum, a) => partialSum + a, 0)
) / grouped[time][currency].length);
}
await PricesRepository.$savePrices(parseInt(time, 10), prices);
++totalInserted;
}
if (totalInserted > 0) {
logger.notice(`Inserted ${totalInserted} ${type === 'day' ? 'dai' : 'hour'}ly historical prices into the db`);
} else {
logger.debug(`Inserted ${totalInserted} ${type === 'day' ? 'dai' : 'hour'}ly historical prices into the db`);
}
}
}
export default new PriceUpdater();

View File

@@ -0,0 +1,63 @@
import axios, { AxiosResponse } from 'axios';
import { SocksProxyAgent } from 'socks-proxy-agent';
import backendInfo from '../api/backend-info';
import config from '../config';
import logger from '../logger';
import * as https from 'https';
export async function query(path): Promise<object | undefined> {
type axiosOptions = {
headers: {
'User-Agent': string
};
timeout: number;
httpsAgent?: https.Agent;
};
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
const axiosOptions: axiosOptions = {
headers: {
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
},
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
};
let retry = 0;
while (retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
try {
if (config.SOCKS5PROXY.ENABLED) {
const socksOptions: any = {
agentOptions: {
keepAlive: true,
},
hostname: config.SOCKS5PROXY.HOST,
port: config.SOCKS5PROXY.PORT
};
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
socksOptions.username = config.SOCKS5PROXY.USERNAME;
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
} else {
// Retry with different tor circuits https://stackoverflow.com/a/64960234
socksOptions.username = `circuit${retry}`;
}
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
}
const data: AxiosResponse = await axios.get(path, axiosOptions);
if (data.statusText === 'error' || !data.data) {
throw new Error(`Could not fetch data from ${path}, Error: ${data.status}`);
}
return data.data;
} catch (e) {
logger.warn(`Could not connect to ${path} (Attempt ${retry + 1}/${config.MEMPOOL.EXTERNAL_MAX_RETRY}). Reason: ` + (e instanceof Error ? e.message : e));
retry++;
}
if (retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
}
}
logger.err(`Could not connect to ${path}. All ${config.MEMPOOL.EXTERNAL_MAX_RETRY} attempts failed`);
return undefined;
}

View File

@@ -3,14 +3,14 @@ import { BlockExtended } from '../mempool.interfaces';
export function prepareBlock(block: any): BlockExtended {
return <BlockExtended>{
id: block.id ?? block.hash, // hash for indexed block
timestamp: block.timestamp ?? block.blockTimestamp, // blockTimestamp for indexed block
timestamp: block.timestamp ?? block.time ?? block.blockTimestamp, // blockTimestamp for indexed block
height: block.height,
version: block.version,
bits: block.bits,
bits: (typeof block.bits === 'string' ? parseInt(block.bits, 16): block.bits),
nonce: block.nonce,
difficulty: block.difficulty,
merkle_root: block.merkle_root,
tx_count: block.tx_count,
merkle_root: block.merkle_root ?? block.merkleroot,
tx_count: block.tx_count ?? block.nTx,
size: block.size,
weight: block.weight,
previousblockhash: block.previousblockhash,
@@ -27,6 +27,7 @@ export function prepareBlock(block: any): BlockExtended {
name: block.pool_name,
slug: block.pool_slug,
} : undefined),
usd: block?.extras?.usd ?? block.usd ?? null,
}
};
}

View File

@@ -0,0 +1,119 @@
var net = require('net');
var IPCheck = module.exports = function(input) {
var self = this;
if (!(self instanceof IPCheck)) {
return new IPCheck(input);
}
self.input = input;
self.parse();
};
IPCheck.prototype.parse = function() {
var self = this;
if (!self.input || typeof self.input !== 'string') return self.valid = false;
var ip;
var pos = self.input.lastIndexOf('/');
if (pos !== -1) {
ip = self.input.substring(0, pos);
self.mask = +self.input.substring(pos + 1);
} else {
ip = self.input;
self.mask = null;
}
self.ipv = net.isIP(ip);
self.valid = !!self.ipv && !isNaN(self.mask);
if (!self.valid) return;
// default mask = 32 for ipv4 and 128 for ipv6
if (self.mask === null) self.mask = self.ipv === 4 ? 32 : 128;
if (self.ipv === 4) {
// difference between ipv4 and ipv6 masks
self.mask += 96;
}
if (self.mask < 0 || self.mask > 128) {
self.valid = false;
return;
}
self.address = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
if(self.ipv === 4){
self.parseIPv4(ip);
}else{
self.parseIPv6(ip);
}
};
IPCheck.prototype.parseIPv4 = function(ip) {
var self = this;
// ipv4 addresses live under ::ffff:0:0
self.address[10] = self.address[11] = 0xff;
var octets = ip.split('.');
for (var i = 0; i < 4; i++) {
self.address[i + 12] = parseInt(octets[i], 10);
}
};
var V6_TRANSITIONAL = /:(\d+\.\d+\.\d+\.\d+)$/;
IPCheck.prototype.parseIPv6 = function(ip) {
var self = this;
var transitionalMatch = V6_TRANSITIONAL.exec(ip);
if(transitionalMatch){
self.parseIPv4(transitionalMatch[1]);
return;
}
var bits = ip.split(':');
if (bits.length < 8) {
ip = ip.replace('::', Array(11 - bits.length).join(':'));
bits = ip.split(':');
}
var j = 0;
for (var i = 0; i < bits.length; i += 1) {
var x = bits[i] ? parseInt(bits[i], 16) : 0;
self.address[j++] = x >> 8;
self.address[j++] = x & 0xff;
}
};
IPCheck.prototype.match = function(cidr) {
var self = this;
if (!(cidr instanceof IPCheck)) cidr = new IPCheck(cidr);
if (!self.valid || !cidr.valid) return false;
var mask = cidr.mask;
var i = 0;
while (mask >= 8) {
if (self.address[i] !== cidr.address[i]) return false;
i++;
mask -= 8;
}
var shift = 8 - mask;
return (self.address[i] >>> shift) === (cidr.address[i] >>> shift);
};
IPCheck.match = function(ip, cidr) {
ip = ip instanceof IPCheck ? ip : new IPCheck(ip);
return ip.match(cidr);
};

5
backend/testSetup.ts Normal file
View File

@@ -0,0 +1,5 @@
jest.mock('./mempool-config.json', () => ({}), { virtual: true });
jest.mock('./src/logger.ts', () => ({}), { virtual: true });
jest.mock('./src/api/rbf-cache.ts', () => ({}), { virtual: true });
jest.mock('./src/api/mempool.ts', () => ({}), { virtual: true });
jest.mock('./src/api/memory-cache.ts', () => ({}), { virtual: true });

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig",
"exclude": ["**/*.test.*", "**/__mocks__/*", "**/__tests__/*"],
"compilerOptions": {
"types": ["node"]
},
}

View File

@@ -2,6 +2,7 @@
"compilerOptions": {
"module": "commonjs",
"target": "esnext",
"types": ["node", "jest"],
"lib": ["es2019", "dom"],
"strict": true,
"noImplicitAny": false,
@@ -11,7 +12,9 @@
"typeRoots": [
"node_modules/@types"
],
"allowSyntheticDefaultImports": true
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"allowJs": true,
},
"include": [
"src/**/*.ts"
@@ -19,4 +22,4 @@
"exclude": [
"dist/**"
]
}
}

View File

@@ -1,137 +0,0 @@
{
"rules": {
"arrow-return-shorthand": true,
"callable-types": true,
"class-name": true,
"comment-format": [
true,
"check-space"
],
"curly": true,
"deprecation": {
"severity": "warn"
},
"eofline": true,
"forin": false,
"import-blacklist": [
true,
"rxjs",
"rxjs/Rx"
],
"import-spacing": true,
"indent": [
true,
"spaces"
],
"interface-over-type-literal": true,
"label-position": true,
"max-line-length": [
true,
140
],
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-arg": true,
"no-bitwise": true,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-super": true,
"no-empty": false,
"no-empty-interface": true,
"no-eval": true,
"no-inferrable-types": false,
"no-misused-new": true,
"no-non-null-assertion": true,
"no-shadowed-variable": true,
"no-string-literal": false,
"no-string-throw": true,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unnecessary-initializer": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"prefer-const": true,
"quotemark": [
true,
"single"
],
"radix": true,
"semicolon": [
true,
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"unified-signatures": true,
"variable-name": false,
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
],
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
],
"no-output-on-prefix": true,
"use-input-property-decorator": true,
"use-output-property-decorator": true,
"use-host-property-decorator": true,
"no-input-rename": true,
"no-output-rename": true,
"use-life-cycle-interface": true,
"use-pipe-transform-interface": true,
"component-class-suffix": true,
"directive-class-suffix": true
}
}

View File

@@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of September 1, 2022.
Signed: WesVleuten

View File

@@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 6, 2022.
Signed: erikarvstedt

3
contributors/junderw.txt Normal file
View File

@@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of August 19, 2022.
Signed: junderw

View File

@@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 25, 2022.
Signed: oleonardolima

View File

@@ -4,6 +4,8 @@ This directory contains the Dockerfiles used to build and release the official i
If you are looking to use these Docker images to deploy your own instance of Mempool, note that they only containerize Mempool's frontend and backend. You will still need to deploy and configure Bitcoin Core and an Electrum Server separately, along with any other utilities specific to your use case (e.g., a reverse proxy, etc). Such configuration is mostly beyond the scope of the Mempool project, so please only proceed if you know what you're doing.
See a video guide of this installation method by k3tan [on BitcoinTV.com](https://bitcointv.com/w/8fpAx6rf5CQ16mMhospwjg).
Jump to a section in this doc:
- [Configure with Bitcoin Core Only](#configure-with-bitcoin-core-only)
- [Configure with Bitcoin Core + Electrum Server](#configure-with-bitcoin-core--electrum-server)
@@ -100,7 +102,9 @@ Below we list all settings from `mempool-config.json` and the corresponding over
"PRICE_FEED_UPDATE_INTERVAL": 600,
"USE_SECOND_NODE_FOR_MINFEE": false,
"EXTERNAL_ASSETS": ["https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json"],
"STDOUT_LOG_MIN_PRIORITY": "info"
"STDOUT_LOG_MIN_PRIORITY": "info",
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json",
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master"
},
```
@@ -124,6 +128,8 @@ Corresponding `docker-compose.yml` overrides:
MEMPOOL_USE_SECOND_NODE_FOR_MINFEE: ""
MEMPOOL_EXTERNAL_ASSETS: ""
MEMPOOL_STDOUT_LOG_MIN_PRIORITY: ""
MEMPOOL_POOLS_JSON_URL: ""
MEMPOOL_POOLS_JSON_TREE_URL: ""
...
```
@@ -233,7 +239,7 @@ Corresponding `docker-compose.yml` overrides:
DATABASE_HOST: ""
DATABASE_PORT: ""
DATABASE_DATABASE: ""
DATABASE_USERAME: ""
DATABASE_USERNAME: ""
DATABASE_PASSWORD: ""
...
```
@@ -344,3 +350,68 @@ Corresponding `docker-compose.yml` overrides:
PRICE_DATA_SERVER_CLEARNET_URL: ""
...
```
<br/>
`mempool-config.json`:
```
"LIGHTNING": {
"ENABLED": false
"BACKEND": "lnd"
"TOPOLOGY_FOLDER": ""
"STATS_REFRESH_INTERVAL": 600
"GRAPH_REFRESH_INTERVAL": 600
"LOGGER_UPDATE_INTERVAL": 30
}
```
Corresponding `docker-compose.yml` overrides:
```
api:
environment:
LIGHTNING_ENABLED: false
LIGHTNING_BACKEND: "lnd"
LIGHTNING_TOPOLOGY_FOLDER: ""
LIGHTNING_STATS_REFRESH_INTERVAL: 600
LIGHTNING_GRAPH_REFRESH_INTERVAL: 600
LIGHTNING_LOGGER_UPDATE_INTERVAL: 30
...
```
<br/>
`mempool-config.json`:
```
"LND": {
"TLS_CERT_PATH": ""
"MACAROON_PATH": ""
"REST_API_URL": "https://localhost:8080"
}
```
Corresponding `docker-compose.yml` overrides:
```
api:
environment:
LND_TLS_CERT_PATH: ""
LND_MACAROON_PATH: ""
LND_REST_API_URL: "https://localhost:8080"
...
```
<br/>
`mempool-config.json`:
```
"CLIGHTNING": {
"SOCKET": ""
}
```
Corresponding `docker-compose.yml` overrides:
```
api:
environment:
CLIGHTNING_SOCKET: ""
...
```

View File

@@ -1,26 +1,23 @@
FROM node:16.15.0-buster-slim AS builder
FROM node:16.16.0-buster-slim AS builder
ARG commitHash
ENV DOCKER_COMMIT_HASH=${commitHash}
ENV MEMPOOL_COMMIT_HASH=${commitHash}
WORKDIR /build
COPY . .
RUN apt-get update
RUN apt-get install -y build-essential python3 pkg-config
RUN npm install
RUN npm run build
RUN npm install --omit=dev --omit=optional
RUN npm run package
FROM node:16.15.0-buster-slim
FROM node:16.16.0-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
RUN chown 1000:1000 ./
COPY --from=builder --chown=1000:1000 /build/package ./package/
COPY --from=builder --chown=1000:1000 /build/mempool-config.json /build/start.sh /build/wait-for-it.sh ./
USER 1000

View File

@@ -19,7 +19,9 @@
"EXTERNAL_RETRY_INTERVAL": __MEMPOOL_EXTERNAL_RETRY_INTERVAL__,
"USER_AGENT": "__MEMPOOL_USER_AGENT__",
"STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
"INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__
"INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__,
"BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__,
"AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__
},
"CORE_RPC": {
"HOST": "__CORE_RPC_HOST__",
@@ -65,6 +67,22 @@
"ENABLED": __BISQ_ENABLED__,
"DATA_PATH": "__BISQ_DATA_PATH__"
},
"LIGHTNING": {
"ENABLED": __LIGHTNING_ENABLED__,
"BACKEND": "__LIGHTNING_BACKEND__",
"STATS_REFRESH_INTERVAL": __LIGHTNING_STATS_REFRESH_INTERVAL__,
"GRAPH_REFRESH_INTERVAL": __LIGHTNING_GRAPH_REFRESH_INTERVAL__,
"LOGGER_UPDATE_INTERVAL": __LIGHTNING_LOGGER_UPDATE_INTERVAL__,
"TOPOLOGY_FOLDER": "__LIGHTNING_TOPOLOGY_FOLDER__"
},
"LND": {
"TLS_CERT_PATH": "__LND_TLS_CERT_PATH__",
"MACAROON_PATH": "__LND_MACAROON_PATH__",
"REST_API_URL": "__LND_REST_API_URL__"
},
"CLIGHTNING": {
"SOCKET": "__CLIGHTNING_SOCKET__"
},
"SOCKS5PROXY": {
"ENABLED": __SOCKS5PROXY_ENABLED__,
"USE_ONION": __SOCKS5PROXY_USE_ONION__,

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