diff --git a/backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts index cf3f75208..589e337cf 100644 --- a/backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -21,6 +21,7 @@ class NodesRoutes { .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) ; } @@ -33,6 +34,39 @@ class NodesRoutes { } } + 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); diff --git a/backend/src/api/explorer/statistics.api.ts b/backend/src/api/explorer/statistics.api.ts index 558ee86fd..cab8bfc29 100644 --- a/backend/src/api/explorer/statistics.api.ts +++ b/backend/src/api/explorer/statistics.api.ts @@ -6,7 +6,8 @@ class StatisticsApi { public async $getStatistics(interval: string | null = null): Promise { interval = Common.getSqlInterval(interval); - let query = `SELECT UNIX_TIMESTAMP(added) AS added, channel_count, total_capacity, tor_nodes, clearnet_nodes, unannounced_nodes + 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) { diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 5ae0c6cb5..6ed7c43f9 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -1,5 +1,5 @@ import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser'; -import { NgModule } from '@angular/core'; +import { ModuleWithProviders, NgModule } from '@angular/core'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AppRoutingModule } from './app-routing.module'; @@ -20,6 +20,23 @@ import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-st import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe'; import { AppPreloadingStrategy } from './app.preloading-strategy'; +const providers = [ + ElectrsApiService, + StateService, + WebsocketService, + AudioService, + SeoService, + OpenGraphService, + StorageService, + EnterpriseService, + LanguageService, + ShortenStringPipe, + FiatShortenerPipe, + CapAddressPipe, + AppPreloadingStrategy, + { provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true } +]; + @NgModule({ declarations: [ AppComponent, @@ -32,22 +49,17 @@ import { AppPreloadingStrategy } from './app.preloading-strategy'; BrowserAnimationsModule, SharedModule, ], - providers: [ - ElectrsApiService, - StateService, - WebsocketService, - AudioService, - SeoService, - OpenGraphService, - StorageService, - EnterpriseService, - LanguageService, - ShortenStringPipe, - FiatShortenerPipe, - CapAddressPipe, - AppPreloadingStrategy, - { provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true } - ], + providers: providers, bootstrap: [AppComponent] }) export class AppModule { } + +@NgModule({}) +export class MempoolSharedModule{ + static forRoot(): ModuleWithProviders { + return { + ngModule: AppModule, + providers: providers + }; + } +} diff --git a/frontend/src/app/bisq/bisq-address/bisq-address.component.ts b/frontend/src/app/bisq/bisq-address/bisq-address.component.ts index 75225d9de..eccc88bc7 100644 --- a/frontend/src/app/bisq/bisq-address/bisq-address.component.ts +++ b/frontend/src/app/bisq/bisq-address/bisq-address.component.ts @@ -1,11 +1,11 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; -import { SeoService } from 'src/app/services/seo.service'; +import { SeoService } from '../../services/seo.service'; import { switchMap, filter, catchError } from 'rxjs/operators'; import { ParamMap, ActivatedRoute } from '@angular/router'; import { Subscription, of } from 'rxjs'; import { BisqTransaction } from '../bisq.interfaces'; import { BisqApiService } from '../bisq-api.service'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-bisq-address', diff --git a/frontend/src/app/bisq/bisq-block/bisq-block.component.ts b/frontend/src/app/bisq/bisq-block/bisq-block.component.ts index 2510ee67f..1bb3a24ab 100644 --- a/frontend/src/app/bisq/bisq-block/bisq-block.component.ts +++ b/frontend/src/app/bisq/bisq-block/bisq-block.component.ts @@ -1,14 +1,14 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; -import { BisqBlock } from 'src/app/bisq/bisq.interfaces'; +import { BisqBlock } from '../../bisq/bisq.interfaces'; import { Location } from '@angular/common'; import { BisqApiService } from '../bisq-api.service'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { Subscription, of } from 'rxjs'; import { switchMap, catchError } from 'rxjs/operators'; -import { SeoService } from 'src/app/services/seo.service'; -import { ElectrsApiService } from 'src/app/services/electrs-api.service'; +import { SeoService } from '../../services/seo.service'; +import { ElectrsApiService } from '../../services/electrs-api.service'; import { HttpErrorResponse } from '@angular/common/http'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-bisq-block', diff --git a/frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.ts b/frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.ts index 1e805188b..8d9ed3c11 100644 --- a/frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.ts +++ b/frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.ts @@ -3,9 +3,9 @@ import { BisqApiService } from '../bisq-api.service'; import { switchMap, map, take, mergeMap, tap } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { BisqBlock, BisqOutput, BisqTransaction } from '../bisq.interfaces'; -import { SeoService } from 'src/app/services/seo.service'; +import { SeoService } from '../../services/seo.service'; import { ActivatedRoute, Router } from '@angular/router'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-bisq-blocks', diff --git a/frontend/src/app/bisq/bisq-dashboard/bisq-dashboard.component.ts b/frontend/src/app/bisq/bisq-dashboard/bisq-dashboard.component.ts index 402c9b6fb..fe36f1b53 100644 --- a/frontend/src/app/bisq/bisq-dashboard/bisq-dashboard.component.ts +++ b/frontend/src/app/bisq/bisq-dashboard/bisq-dashboard.component.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { Observable, combineLatest, BehaviorSubject, of } from 'rxjs'; import { map, share, switchMap } from 'rxjs/operators'; -import { SeoService } from 'src/app/services/seo.service'; -import { StateService } from 'src/app/services/state.service'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { SeoService } from '../../services/seo.service'; +import { StateService } from '../../services/state.service'; +import { WebsocketService } from '../../services/websocket.service'; import { BisqApiService } from '../bisq-api.service'; import { Trade } from '../bisq.interfaces'; diff --git a/frontend/src/app/bisq/bisq-main-dashboard/bisq-main-dashboard.component.ts b/frontend/src/app/bisq/bisq-main-dashboard/bisq-main-dashboard.component.ts index a45bb138e..d1b8480f7 100644 --- a/frontend/src/app/bisq/bisq-main-dashboard/bisq-main-dashboard.component.ts +++ b/frontend/src/app/bisq/bisq-main-dashboard/bisq-main-dashboard.component.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { Observable, combineLatest, BehaviorSubject, of } from 'rxjs'; import { map, share, switchMap } from 'rxjs/operators'; -import { SeoService } from 'src/app/services/seo.service'; -import { StateService } from 'src/app/services/state.service'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { SeoService } from '../../services/seo.service'; +import { StateService } from '../../services/state.service'; +import { WebsocketService } from '../../services/websocket.service'; import { BisqApiService } from '../bisq-api.service'; import { Trade } from '../bisq.interfaces'; diff --git a/frontend/src/app/bisq/bisq-market/bisq-market.component.ts b/frontend/src/app/bisq/bisq-market/bisq-market.component.ts index 90832122f..fb5967c63 100644 --- a/frontend/src/app/bisq/bisq-market/bisq-market.component.ts +++ b/frontend/src/app/bisq/bisq-market/bisq-market.component.ts @@ -3,8 +3,8 @@ import { FormBuilder, FormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { combineLatest, merge, Observable, of } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; -import { SeoService } from 'src/app/services/seo.service'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { SeoService } from '../../services/seo.service'; +import { WebsocketService } from '../../services/websocket.service'; import { BisqApiService } from '../bisq-api.service'; import { OffersMarket, Trade } from '../bisq.interfaces'; diff --git a/frontend/src/app/bisq/bisq-stats/bisq-stats.component.ts b/frontend/src/app/bisq/bisq-stats/bisq-stats.component.ts index 11064b5fe..5ec5964b4 100644 --- a/frontend/src/app/bisq/bisq-stats/bisq-stats.component.ts +++ b/frontend/src/app/bisq/bisq-stats/bisq-stats.component.ts @@ -1,9 +1,9 @@ import { Component, OnInit } from '@angular/core'; import { BisqApiService } from '../bisq-api.service'; import { BisqStats } from '../bisq.interfaces'; -import { SeoService } from 'src/app/services/seo.service'; -import { StateService } from 'src/app/services/state.service'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { SeoService } from '../../services/seo.service'; +import { StateService } from '../../services/state.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-bisq-stats', diff --git a/frontend/src/app/bisq/bisq-transaction-details/bisq-transaction-details.component.ts b/frontend/src/app/bisq/bisq-transaction-details/bisq-transaction-details.component.ts index d10d0507e..9728372c3 100644 --- a/frontend/src/app/bisq/bisq-transaction-details/bisq-transaction-details.component.ts +++ b/frontend/src/app/bisq/bisq-transaction-details/bisq-transaction-details.component.ts @@ -1,5 +1,5 @@ import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core'; -import { BisqTransaction } from 'src/app/bisq/bisq.interfaces'; +import { BisqTransaction } from '../../bisq/bisq.interfaces'; @Component({ selector: 'app-bisq-transaction-details', diff --git a/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.ts b/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.ts index c736a2fa9..fb30fc59f 100644 --- a/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.ts +++ b/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.ts @@ -1,15 +1,15 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; -import { BisqTransaction } from 'src/app/bisq/bisq.interfaces'; +import { BisqTransaction } from '../../bisq/bisq.interfaces'; import { switchMap, map, catchError } from 'rxjs/operators'; import { of, Observable, Subscription } from 'rxjs'; -import { StateService } from 'src/app/services/state.service'; -import { Block, Transaction } from 'src/app/interfaces/electrs.interface'; +import { StateService } from '../../services/state.service'; +import { Block, Transaction } from '../../interfaces/electrs.interface'; import { BisqApiService } from '../bisq-api.service'; -import { SeoService } from 'src/app/services/seo.service'; -import { ElectrsApiService } from 'src/app/services/electrs-api.service'; +import { SeoService } from '../../services/seo.service'; +import { ElectrsApiService } from '../../services/electrs-api.service'; import { HttpErrorResponse } from '@angular/common/http'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-bisq-transaction', diff --git a/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts b/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts index d0a2ba3c5..9c58577e3 100644 --- a/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts +++ b/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts @@ -4,11 +4,11 @@ import { BisqTransaction, BisqOutput } from '../bisq.interfaces'; import { Observable, Subscription } from 'rxjs'; import { switchMap, map, tap } from 'rxjs/operators'; import { BisqApiService } from '../bisq-api.service'; -import { SeoService } from 'src/app/services/seo.service'; +import { SeoService } from '../../services/seo.service'; import { FormGroup, FormBuilder } from '@angular/forms'; import { Router, ActivatedRoute } from '@angular/router'; -import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from 'src/app/components/ngx-bootstrap-multiselect/types' -import { WebsocketService } from 'src/app/services/websocket.service'; +import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from '../../components/ngx-bootstrap-multiselect/types' +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-bisq-transactions', diff --git a/frontend/src/app/bisq/bisq-transfers/bisq-transfers.component.ts b/frontend/src/app/bisq/bisq-transfers/bisq-transfers.component.ts index 2fa805c61..4346f15d3 100644 --- a/frontend/src/app/bisq/bisq-transfers/bisq-transfers.component.ts +++ b/frontend/src/app/bisq/bisq-transfers/bisq-transfers.component.ts @@ -1,9 +1,9 @@ import { Component, OnInit, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core'; -import { BisqTransaction } from 'src/app/bisq/bisq.interfaces'; -import { StateService } from 'src/app/services/state.service'; +import { BisqTransaction } from '../../bisq/bisq.interfaces'; +import { StateService } from '../../services/state.service'; import { map } from 'rxjs/operators'; import { Observable } from 'rxjs'; -import { Block } from 'src/app/interfaces/electrs.interface'; +import { Block } from '../../interfaces/electrs.interface'; @Component({ selector: 'app-bisq-transfers', diff --git a/frontend/src/app/bisq/bsq-amount/bsq-amount.component.ts b/frontend/src/app/bisq/bsq-amount/bsq-amount.component.ts index 263b9d7f7..a3dd10e81 100644 --- a/frontend/src/app/bisq/bsq-amount/bsq-amount.component.ts +++ b/frontend/src/app/bisq/bsq-amount/bsq-amount.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; -import { StateService } from 'src/app/services/state.service'; +import { StateService } from '../../services/state.service'; import { Observable } from 'rxjs'; @Component({ diff --git a/frontend/src/app/components/about/about.component.ts b/frontend/src/app/components/about/about.component.ts index 693be2d34..d26efb411 100644 --- a/frontend/src/app/components/about/about.component.ts +++ b/frontend/src/app/components/about/about.component.ts @@ -1,13 +1,13 @@ import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core'; import { WebsocketService } from '../../services/websocket.service'; -import { SeoService } from 'src/app/services/seo.service'; -import { StateService } from 'src/app/services/state.service'; +import { SeoService } from '../../services/seo.service'; +import { StateService } from '../../services/state.service'; import { Observable } from 'rxjs'; -import { ApiService } from 'src/app/services/api.service'; -import { IBackendInfo } from 'src/app/interfaces/websocket.interface'; +import { ApiService } from '../../services/api.service'; +import { IBackendInfo } from '../../interfaces/websocket.interface'; import { Router } from '@angular/router'; import { map } from 'rxjs/operators'; -import { ITranslators } from 'src/app/interfaces/node-api.interface'; +import { ITranslators } from '../../interfaces/node-api.interface'; @Component({ selector: 'app-about', diff --git a/frontend/src/app/components/address-labels/address-labels.component.html b/frontend/src/app/components/address-labels/address-labels.component.html index 353e733ae..dfc6647f4 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.html +++ b/frontend/src/app/components/address-labels/address-labels.component.html @@ -1,9 +1,16 @@ - - {{ label }} - + +
+ +   +
+
- - + + + + + + + diff --git a/frontend/src/app/components/clipboard/clipboard.component.ts b/frontend/src/app/components/clipboard/clipboard.component.ts index e6a5c0e6e..7fbffdca3 100644 --- a/frontend/src/app/components/clipboard/clipboard.component.ts +++ b/frontend/src/app/components/clipboard/clipboard.component.ts @@ -11,6 +11,8 @@ import * as tlite from 'tlite'; export class ClipboardComponent implements AfterViewInit { @ViewChild('btn') btn: ElementRef; @ViewChild('buttonWrapper') buttonWrapper: ElementRef; + @Input() button = false; + @Input() class = 'btn btn-secondary ml-1'; @Input() size: 'small' | 'normal' = 'normal'; @Input() text: string; @Input() leftPadding = true; diff --git a/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.components.ts b/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.components.ts index f12277f72..7db1367ea 100644 --- a/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.components.ts +++ b/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.components.ts @@ -1,10 +1,10 @@ import { Component, Inject, LOCALE_ID, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiService } from 'src/app/services/api.service'; +import { ApiService } from '../../services/api.service'; import { formatNumber } from '@angular/common'; -import { selectPowerOfTen } from 'src/app/bitcoin.utils'; -import { StateService } from 'src/app/services/state.service'; +import { selectPowerOfTen } from '../../bitcoin.utils'; +import { StateService } from '../../services/state.service'; @Component({ selector: 'app-difficulty-adjustments-table', diff --git a/frontend/src/app/components/fees-box/fees-box.component.ts b/frontend/src/app/components/fees-box/fees-box.component.ts index 20fe42647..48098db7b 100644 --- a/frontend/src/app/components/fees-box/fees-box.component.ts +++ b/frontend/src/app/components/fees-box/fees-box.component.ts @@ -1,8 +1,8 @@ import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; -import { StateService } from 'src/app/services/state.service'; +import { StateService } from '../../services/state.service'; import { Observable } from 'rxjs'; -import { Recommendedfees } from 'src/app/interfaces/websocket.interface'; -import { feeLevels, mempoolFeeColors } from 'src/app/app.constants'; +import { Recommendedfees } from '../../interfaces/websocket.interface'; +import { feeLevels, mempoolFeeColors } from '../../app.constants'; import { tap } from 'rxjs/operators'; @Component({ diff --git a/frontend/src/app/components/footer/footer.component.ts b/frontend/src/app/components/footer/footer.component.ts index dbaa478d7..5e5b1f52a 100644 --- a/frontend/src/app/components/footer/footer.component.ts +++ b/frontend/src/app/components/footer/footer.component.ts @@ -1,8 +1,8 @@ import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; -import { StateService } from 'src/app/services/state.service'; +import { StateService } from '../../services/state.service'; import { Observable, combineLatest } from 'rxjs'; import { map } from 'rxjs/operators'; -import { MempoolInfo } from 'src/app/interfaces/websocket.interface'; +import { MempoolInfo } from '../../interfaces/websocket.interface'; interface MempoolBlocksData { blocks: number; diff --git a/frontend/src/app/components/graphs/graphs.component.ts b/frontend/src/app/components/graphs/graphs.component.ts index 1d3a4e2ae..050b69848 100644 --- a/frontend/src/app/components/graphs/graphs.component.ts +++ b/frontend/src/app/components/graphs/graphs.component.ts @@ -1,6 +1,6 @@ -import { Component, OnInit } from "@angular/core"; -import { StateService } from "src/app/services/state.service"; -import { WebsocketService } from "src/app/services/websocket.service"; +import { Component, OnInit } from '@angular/core'; +import { StateService } from '../../services/state.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-graphs', diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts index 8952a27ce..50479f5d1 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts @@ -2,16 +2,16 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, H import { EChartsOption, graphic } from 'echarts'; import { Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from 'src/app/services/api.service'; -import { SeoService } from 'src/app/services/seo.service'; +import { ApiService } from '../../services/api.service'; +import { SeoService } from '../../services/seo.service'; import { formatNumber } from '@angular/common'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { selectPowerOfTen } from 'src/app/bitcoin.utils'; -import { StorageService } from 'src/app/services/storage.service'; -import { MiningService } from 'src/app/services/mining.service'; -import { download } from 'src/app/shared/graphs.utils'; +import { selectPowerOfTen } from '../../bitcoin.utils'; +import { StorageService } from '../../services/storage.service'; +import { MiningService } from '../../services/mining.service'; +import { download } from '../../shared/graphs.utils'; import { ActivatedRoute } from '@angular/router'; -import { StateService } from 'src/app/services/state.service'; +import { StateService } from '../../services/state.service'; @Component({ selector: 'app-hashrate-chart', diff --git a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts index 716fc3216..dc0d5b5ed 100644 --- a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts +++ b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts @@ -2,13 +2,13 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, L import { EChartsOption } from 'echarts'; import { Observable } from 'rxjs'; import { delay, map, retryWhen, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from 'src/app/services/api.service'; -import { SeoService } from 'src/app/services/seo.service'; +import { ApiService } from '../../services/api.service'; +import { SeoService } from '../../services/seo.service'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { poolsColor } from 'src/app/app.constants'; -import { StorageService } from 'src/app/services/storage.service'; -import { MiningService } from 'src/app/services/mining.service'; -import { download } from 'src/app/shared/graphs.utils'; +import { poolsColor } from '../../app.constants'; +import { StorageService } from '../../services/storage.service'; +import { MiningService } from '../../services/mining.service'; +import { download } from '../../shared/graphs.utils'; import { ActivatedRoute } from '@angular/router'; @Component({ diff --git a/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts b/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts index 33ae2d320..d721469b7 100644 --- a/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts +++ b/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts @@ -1,8 +1,8 @@ import { Component, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, OnInit } from '@angular/core'; import { EChartsOption } from 'echarts'; import { OnChanges } from '@angular/core'; -import { StorageService } from 'src/app/services/storage.service'; -import { download, formatterXAxis, formatterXAxisLabel } from 'src/app/shared/graphs.utils'; +import { StorageService } from '../../services/storage.service'; +import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/graphs.utils'; import { formatNumber } from '@angular/common'; @Component({ diff --git a/frontend/src/app/components/language-selector/language-selector.component.ts b/frontend/src/app/components/language-selector/language-selector.component.ts index a2b10b7db..9fc2be6f7 100644 --- a/frontend/src/app/components/language-selector/language-selector.component.ts +++ b/frontend/src/app/components/language-selector/language-selector.component.ts @@ -1,8 +1,8 @@ import { DOCUMENT } from '@angular/common'; import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { languages } from 'src/app/app.constants'; -import { LanguageService } from 'src/app/services/language.service'; +import { languages } from '../../app.constants'; +import { LanguageService } from '../../services/language.service'; @Component({ selector: 'app-language-selector', diff --git a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts index 22a351068..d78cd457e 100644 --- a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts +++ b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts @@ -1,8 +1,8 @@ import { Component, OnInit } from '@angular/core'; import { Env, StateService } from '../../services/state.service'; import { merge, Observable, of} from 'rxjs'; -import { LanguageService } from 'src/app/services/language.service'; -import { EnterpriseService } from 'src/app/services/enterprise.service'; +import { LanguageService } from '../../services/language.service'; +import { EnterpriseService } from '../../services/enterprise.service'; @Component({ selector: 'app-liquid-master-page', diff --git a/frontend/src/app/components/loading-indicator/loading-indicator.component.ts b/frontend/src/app/components/loading-indicator/loading-indicator.component.ts index 3f59c2701..83a5ccc72 100644 --- a/frontend/src/app/components/loading-indicator/loading-indicator.component.ts +++ b/frontend/src/app/components/loading-indicator/loading-indicator.component.ts @@ -1,8 +1,8 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { StateService } from 'src/app/services/state.service'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { StateService } from '../../services/state.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-loading-indicator', diff --git a/frontend/src/app/components/master-page-preview/master-page-preview.component.ts b/frontend/src/app/components/master-page-preview/master-page-preview.component.ts index 61a392b5e..03a6a1ebb 100644 --- a/frontend/src/app/components/master-page-preview/master-page-preview.component.ts +++ b/frontend/src/app/components/master-page-preview/master-page-preview.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { StateService } from '../../services/state.service'; import { Observable, merge, of } from 'rxjs'; -import { LanguageService } from 'src/app/services/language.service'; +import { LanguageService } from '../../services/language.service'; @Component({ selector: 'app-master-page-preview', diff --git a/frontend/src/app/components/master-page/master-page.component.ts b/frontend/src/app/components/master-page/master-page.component.ts index 6ef6b86b2..994f0412f 100644 --- a/frontend/src/app/components/master-page/master-page.component.ts +++ b/frontend/src/app/components/master-page/master-page.component.ts @@ -1,8 +1,8 @@ import { Component, Inject, OnInit } from '@angular/core'; import { Env, StateService } from '../../services/state.service'; import { Observable, merge, of } from 'rxjs'; -import { LanguageService } from 'src/app/services/language.service'; -import { EnterpriseService } from 'src/app/services/enterprise.service'; +import { LanguageService } from '../../services/language.service'; +import { EnterpriseService } from '../../services/enterprise.service'; @Component({ selector: 'app-master-page', diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts index bd78b13a9..b7fb65753 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts @@ -1,12 +1,12 @@ import { Component, ComponentRef, ViewChild, HostListener, Input, Output, EventEmitter, OnDestroy, OnChanges, ChangeDetectionStrategy, AfterViewInit } from '@angular/core'; -import { StateService } from 'src/app/services/state.service'; -import { MempoolBlockDelta, TransactionStripped } from 'src/app/interfaces/websocket.interface'; -import { BlockOverviewGraphComponent } from 'src/app/components/block-overview-graph/block-overview-graph.component'; +import { StateService } from '../../services/state.service'; +import { MempoolBlockDelta, TransactionStripped } from '../../interfaces/websocket.interface'; +import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component'; import { Subscription, BehaviorSubject, merge, of } from 'rxjs'; import { switchMap, filter } from 'rxjs/operators'; -import { WebsocketService } from 'src/app/services/websocket.service'; -import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; +import { WebsocketService } from '../../services/websocket.service'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { Router } from '@angular/router'; @Component({ diff --git a/frontend/src/app/components/mempool-block/mempool-block.component.ts b/frontend/src/app/components/mempool-block/mempool-block.component.ts index 75e171f2b..b9bdc55bb 100644 --- a/frontend/src/app/components/mempool-block/mempool-block.component.ts +++ b/frontend/src/app/components/mempool-block/mempool-block.component.ts @@ -1,11 +1,11 @@ import { Component, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; -import { StateService } from 'src/app/services/state.service'; +import { StateService } from '../../services/state.service'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { switchMap, map, tap, filter } from 'rxjs/operators'; -import { MempoolBlock, TransactionStripped } from 'src/app/interfaces/websocket.interface'; +import { MempoolBlock, TransactionStripped } from '../../interfaces/websocket.interface'; import { Observable, BehaviorSubject } from 'rxjs'; -import { SeoService } from 'src/app/services/seo.service'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { SeoService } from '../../services/seo.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-mempool-block', diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts index 0019c8a44..4202330b0 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -1,14 +1,14 @@ import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input } from '@angular/core'; import { Subscription, Observable, fromEvent, merge, of, combineLatest, timer } from 'rxjs'; -import { MempoolBlock } from 'src/app/interfaces/websocket.interface'; -import { StateService } from 'src/app/services/state.service'; +import { MempoolBlock } from '../../interfaces/websocket.interface'; +import { StateService } from '../../services/state.service'; import { Router } from '@angular/router'; import { take, map, switchMap } from 'rxjs/operators'; -import { feeLevels, mempoolFeeColors } from 'src/app/app.constants'; -import { specialBlocks } from 'src/app/app.constants'; -import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; +import { feeLevels, mempoolFeeColors } from '../../app.constants'; +import { specialBlocks } from '../../app.constants'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { Location } from '@angular/common'; -import { DifficultyAdjustment } from 'src/app/interfaces/node-api.interface'; +import { DifficultyAdjustment } from '../../interfaces/node-api.interface'; @Component({ selector: 'app-mempool-blocks', diff --git a/frontend/src/app/components/mempool-graph/mempool-graph.component.ts b/frontend/src/app/components/mempool-graph/mempool-graph.component.ts index 04c7ddf69..989fa141e 100644 --- a/frontend/src/app/components/mempool-graph/mempool-graph.component.ts +++ b/frontend/src/app/components/mempool-graph/mempool-graph.component.ts @@ -1,12 +1,12 @@ import { Component, OnInit, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, OnChanges } from '@angular/core'; -import { VbytesPipe } from 'src/app/shared/pipes/bytes-pipe/vbytes.pipe'; +import { VbytesPipe } from '../../shared/pipes/bytes-pipe/vbytes.pipe'; import { formatNumber } from '@angular/common'; -import { OptimizedMempoolStats } from 'src/app/interfaces/node-api.interface'; -import { StateService } from 'src/app/services/state.service'; -import { StorageService } from 'src/app/services/storage.service'; +import { OptimizedMempoolStats } from '../../interfaces/node-api.interface'; +import { StateService } from '../../services/state.service'; +import { StorageService } from '../../services/storage.service'; import { EChartsOption } from 'echarts'; -import { feeLevels, chartColors } from 'src/app/app.constants'; -import { download, formatterXAxis, formatterXAxisLabel } from 'src/app/shared/graphs.utils'; +import { feeLevels, chartColors } from '../../app.constants'; +import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/graphs.utils'; @Component({ selector: 'app-mempool-graph', diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts index 15ffe2c87..df4713374 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { SeoService } from 'src/app/services/seo.service'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { SeoService } from '../../services/seo.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-mining-dashboard', diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts index 820699d2b..57542fd30 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts @@ -4,15 +4,15 @@ import { ActivatedRoute, Router } from '@angular/router'; import { EChartsOption, PieSeriesOption } from 'echarts'; import { concat, Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { SinglePoolStats } from 'src/app/interfaces/node-api.interface'; -import { SeoService } from 'src/app/services/seo.service'; +import { SinglePoolStats } from '../../interfaces/node-api.interface'; +import { SeoService } from '../../services/seo.service'; import { StorageService } from '../..//services/storage.service'; import { MiningService, MiningStats } from '../../services/mining.service'; import { StateService } from '../../services/state.service'; -import { chartColors, poolsColor } from 'src/app/app.constants'; -import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; -import { download } from 'src/app/shared/graphs.utils'; -import { isMobile } from 'src/app/shared/common.utils'; +import { chartColors, poolsColor } from '../../app.constants'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; +import { download } from '../../shared/graphs.utils'; +import { isMobile } from '../../shared/common.utils'; @Component({ selector: 'app-pool-ranking', diff --git a/frontend/src/app/components/pool/pool-preview.component.html b/frontend/src/app/components/pool/pool-preview.component.html new file mode 100644 index 000000000..93e65aeae --- /dev/null +++ b/frontend/src/app/components/pool/pool-preview.component.html @@ -0,0 +1,34 @@ +
+ + mining pool + +
+
+

{{ poolStats.pool.name }}

+
+
+ + +
+
+
+
+
+
Tags
+
{{ poolStats.pool.regexes }}
+
+
+
Hashrate
+
{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}
+
+
+
+
+
+
+
+ + +
~
+
\ No newline at end of file diff --git a/frontend/src/app/components/pool/pool-preview.component.scss b/frontend/src/app/components/pool/pool-preview.component.scss new file mode 100644 index 000000000..533bac4af --- /dev/null +++ b/frontend/src/app/components/pool/pool-preview.component.scss @@ -0,0 +1,78 @@ +.stats { + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-start; + width: 100%; + max-width: 100%; + margin: 15px 0; + font-size: 32px; + overflow: hidden; + + .stat-box { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: baseline; + justify-content: space-between; + width: 100%; + margin-left: 15px; + background: #181b2d; + padding: 0.75rem; + width: 0; + flex-grow: 1; + + &:first-child { + margin-left: 0; + } + + .label { + flex-shrink: 0; + flex-grow: 0; + margin-right: 1em; + } + .data { + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } +} + +.chart { + width: 100%; + height: 315px; + background: #181b2d; +} + +.row { + margin-right: 0; +} + +.full-width-row { + padding-left: 15px; + flex-wrap: nowrap; +} + +.logo-wrapper { + position: relative; + width: 62px; + height: 62px; + margin-left: 1em; + + img { + position: absolute; + right: 0; + top: 0; + background: #24273e; + + &.noimg { + opacity: 0; + } + } +} + +::ng-deep .symbol { + font-size: 24px; +} diff --git a/frontend/src/app/components/pool/pool-preview.component.ts b/frontend/src/app/components/pool/pool-preview.component.ts new file mode 100644 index 000000000..277bacb33 --- /dev/null +++ b/frontend/src/app/components/pool/pool-preview.component.ts @@ -0,0 +1,187 @@ +import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { EChartsOption, graphic } from 'echarts'; +import { Observable, of } from 'rxjs'; +import { map, switchMap, catchError } from 'rxjs/operators'; +import { PoolStat } from '../../interfaces/node-api.interface'; +import { ApiService } from '../../services/api.service'; +import { StateService } from '../../services/state.service'; +import { formatNumber } from '@angular/common'; +import { SeoService } from '../../services/seo.service'; +import { OpenGraphService } from '../../services/opengraph.service'; + +@Component({ + selector: 'app-pool-preview', + templateUrl: './pool-preview.component.html', + styleUrls: ['./pool-preview.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class PoolPreviewComponent implements OnInit { + formatNumber = formatNumber; + poolStats$: Observable; + isLoading = true; + imageLoaded = false; + lastImgSrc: string = ''; + + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + + slug: string = undefined; + + constructor( + @Inject(LOCALE_ID) public locale: string, + private apiService: ApiService, + private route: ActivatedRoute, + public stateService: StateService, + private seoService: SeoService, + private openGraphService: OpenGraphService, + ) { + } + + ngOnInit(): void { + this.poolStats$ = this.route.params.pipe(map((params) => params.slug)) + .pipe( + switchMap((slug: any) => { + this.isLoading = true; + this.imageLoaded = false; + this.slug = slug; + this.openGraphService.waitFor('pool-hash-' + this.slug); + this.openGraphService.waitFor('pool-stats-' + this.slug); + this.openGraphService.waitFor('pool-chart-' + this.slug); + this.openGraphService.waitFor('pool-img-' + this.slug); + return this.apiService.getPoolHashrate$(this.slug) + .pipe( + switchMap((data) => { + this.isLoading = false; + this.prepareChartOptions(data.map(val => [val.timestamp * 1000, val.avgHashrate])); + this.openGraphService.waitOver('pool-hash-' + this.slug); + return [slug]; + }), + catchError(() => { + this.isLoading = false; + this.openGraphService.fail('pool-hash-' + this.slug); + return of([slug]); + }) + ); + }), + switchMap((slug) => { + return this.apiService.getPoolStats$(slug).pipe( + catchError(() => { + this.isLoading = false; + this.openGraphService.fail('pool-stats-' + this.slug); + return of(null); + }) + ); + }), + map((poolStats) => { + if (poolStats == null) { + return null; + } + + this.seoService.setTitle(poolStats.pool.name); + let regexes = '"'; + for (const regex of poolStats.pool.regexes) { + regexes += regex + '", "'; + } + poolStats.pool.regexes = regexes.slice(0, -3); + poolStats.pool.addresses = poolStats.pool.addresses; + + if (poolStats.reportedHashrate) { + poolStats.luck = poolStats.estimatedHashrate / poolStats.reportedHashrate * 100; + } + + this.openGraphService.waitOver('pool-stats-' + this.slug); + + const logoSrc = `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'; + if (logoSrc === this.lastImgSrc) { + this.openGraphService.waitOver('pool-img-' + this.slug); + } + this.lastImgSrc = logoSrc; + return Object.assign({ + logo: logoSrc + }, poolStats); + }), + catchError(() => { + this.isLoading = false; + this.openGraphService.fail('pool-stats-' + this.slug); + return of(null); + }) + ); + } + + prepareChartOptions(data) { + let title: object; + if (data.length === 0) { + title = { + textStyle: { + color: 'grey', + fontSize: 15 + }, + text: $localize`:@@23555386d8af1ff73f297e89dd4af3f4689fb9dd:Indexing blocks`, + left: 'center', + top: 'center' + }; + } + + this.chartOptions = { + title: title, + animation: false, + color: [ + new graphic.LinearGradient(0, 0, 0, 0.65, [ + { offset: 0, color: '#F4511E' }, + { offset: 0.25, color: '#FB8C00' }, + { offset: 0.5, color: '#FFB300' }, + { offset: 0.75, color: '#FDD835' }, + { offset: 1, color: '#7CB342' } + ]), + '#D81B60', + ], + grid: { + left: 15, + right: 15, + bottom: 15, + top: 15, + show: false, + }, + xAxis: data.length === 0 ? undefined : { + type: 'time', + show: false, + }, + yAxis: data.length === 0 ? undefined : [ + { + type: 'value', + show: false, + }, + ], + series: data.length === 0 ? undefined : [ + { + zlevel: 0, + name: 'Hashrate', + showSymbol: false, + symbol: 'none', + data: data, + type: 'line', + lineStyle: { + width: 4, + }, + }, + ], + }; + } + + onChartReady(): void { + this.openGraphService.waitOver('pool-chart-' + this.slug); + } + + onImageLoad(): void { + this.imageLoaded = true; + this.openGraphService.waitOver('pool-img-' + this.slug); + } + + onImageFail(): void { + this.imageLoaded = false; + this.openGraphService.waitOver('pool-img-' + this.slug); + } +} diff --git a/frontend/src/app/components/pool/pool.component.ts b/frontend/src/app/components/pool/pool.component.ts index 7a37bf7bd..56b8bd392 100644 --- a/frontend/src/app/components/pool/pool.component.ts +++ b/frontend/src/app/components/pool/pool.component.ts @@ -3,12 +3,12 @@ import { ActivatedRoute } from '@angular/router'; import { EChartsOption, graphic } from 'echarts'; import { BehaviorSubject, Observable, timer } from 'rxjs'; import { distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; -import { BlockExtended, PoolStat } from 'src/app/interfaces/node-api.interface'; -import { ApiService } from 'src/app/services/api.service'; -import { StateService } from 'src/app/services/state.service'; -import { selectPowerOfTen } from 'src/app/bitcoin.utils'; +import { BlockExtended, PoolStat } from '../../interfaces/node-api.interface'; +import { ApiService } from '../../services/api.service'; +import { StateService } from '../../services/state.service'; +import { selectPowerOfTen } from '../../bitcoin.utils'; import { formatNumber } from '@angular/common'; -import { SeoService } from 'src/app/services/seo.service'; +import { SeoService } from '../../services/seo.service'; @Component({ selector: 'app-pool', diff --git a/frontend/src/app/components/push-transaction/push-transaction.component.ts b/frontend/src/app/components/push-transaction/push-transaction.component.ts index 294f0591a..a4eb375a6 100644 --- a/frontend/src/app/components/push-transaction/push-transaction.component.ts +++ b/frontend/src/app/components/push-transaction/push-transaction.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { ApiService } from 'src/app/services/api.service'; +import { ApiService } from '../../services/api.service'; @Component({ selector: 'app-push-transaction', diff --git a/frontend/src/app/components/qrcode/qrcode.component.ts b/frontend/src/app/components/qrcode/qrcode.component.ts index 923e66fd8..e8ebac904 100644 --- a/frontend/src/app/components/qrcode/qrcode.component.ts +++ b/frontend/src/app/components/qrcode/qrcode.component.ts @@ -1,6 +1,6 @@ import { Component, Input, AfterViewInit, ViewChild, ElementRef, ChangeDetectionStrategy } from '@angular/core'; import * as QRCode from 'qrcode'; -import { StateService } from 'src/app/services/state.service'; +import { StateService } from '../../services/state.service'; @Component({ selector: 'app-qrcode', diff --git a/frontend/src/app/components/reward-stats/reward-stats.component.ts b/frontend/src/app/components/reward-stats/reward-stats.component.ts index c92ac757d..1eda26cce 100644 --- a/frontend/src/app/components/reward-stats/reward-stats.component.ts +++ b/frontend/src/app/components/reward-stats/reward-stats.component.ts @@ -1,8 +1,8 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { concat, Observable } from 'rxjs'; import { map, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from 'src/app/services/api.service'; -import { StateService } from 'src/app/services/state.service'; +import { ApiService } from '../../services/api.service'; +import { StateService } from '../../services/state.service'; @Component({ selector: 'app-reward-stats', diff --git a/frontend/src/app/components/search-form/search-form.component.html b/frontend/src/app/components/search-form/search-form.component.html index 8badcc3cf..f8cb05d1c 100644 --- a/frontend/src/app/components/search-form/search-form.component.html +++ b/frontend/src/app/components/search-form/search-form.component.html @@ -3,7 +3,7 @@
- +
diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts index 712f7438c..3cff7c188 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -1,13 +1,13 @@ import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; -import { AssetsService } from 'src/app/services/assets.service'; -import { StateService } from 'src/app/services/state.service'; +import { AssetsService } from '../../services/assets.service'; +import { StateService } from '../../services/state.service'; import { Observable, of, Subject, zip, BehaviorSubject } from 'rxjs'; import { debounceTime, distinctUntilChanged, switchMap, catchError, map } from 'rxjs/operators'; -import { ElectrsApiService } from 'src/app/services/electrs-api.service'; -import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; -import { ApiService } from 'src/app/services/api.service'; +import { ElectrsApiService } from '../../services/electrs-api.service'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; +import { ApiService } from '../../services/api.service'; import { SearchResultsComponent } from './search-results/search-results.component'; @Component({ @@ -74,6 +74,7 @@ export class SearchFormComponent implements OnInit { switchMap((text) => { if (!text.length) { return of([ + '', [], { nodes: [], @@ -84,11 +85,14 @@ export class SearchFormComponent implements OnInit { this.isTypeaheading$.next(true); if (!this.stateService.env.LIGHTNING) { return zip( + of(text), this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))), - [{ nodes: [], channels: [] }] + [{ nodes: [], channels: [] }], + of(this.regexBlockheight.test(text)), ); } return zip( + of(text), this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))), this.apiService.lightningSearch$(text).pipe(catchError(() => of({ nodes: [], @@ -102,10 +106,12 @@ export class SearchFormComponent implements OnInit { return result[0].map((address: string) => 'B' + address); } return { - addresses: result[0], - nodes: result[1].nodes, - channels: result[1].channels, - totalResults: result[0].length + result[1].nodes.length + result[1].channels.length, + searchText: result[0], + blockHeight: this.regexBlockheight.test(result[0]) ? [parseInt(result[0], 10)] : [], + addresses: result[1], + nodes: result[2].nodes, + channels: result[2].channels, + totalResults: result[1].length + result[2].nodes.length + result[2].channels.length, }; }) ); @@ -121,6 +127,8 @@ export class SearchFormComponent implements OnInit { selectedResult(result: any) { if (typeof result === 'string') { this.search(result); + } else if (typeof result === 'number') { + this.navigate('/block/', result.toString()); } else if (result.alias) { this.navigate('/lightning/node/', result.public_key); } else if (result.short_id) { diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.html b/frontend/src/app/components/search-form/search-results/search-results.component.html index cc289ddac..9ed829aff 100644 --- a/frontend/src/app/components/search-form/search-results/search-results.component.html +++ b/frontend/src/app/components/search-form/search-results/search-results.component.html @@ -1,25 +1,31 @@ - -
-
- - - - - - - - - - - - - - - - - - - - - - - -
Fee rate - {{ channel.fee_rate ?? '-' }} ppm ({{ channel.fee_rate / 10000 | number }}%) -
Base fee - -
Min HTLC - -
Max HTLC - -
Timelock delta - -
-
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Fee rate + + {{ channel.fee_rate !== null ? (channel.fee_rate | amountShortener : 2 : undefined : true) : '-' }} ppm {{ channel.fee_rate !== null ? '(' + (channel.fee_rate / 10000 | amountShortener : 2 : undefined : true) + '%)' : '' }} + + + {{ channel.fee_rate !== null ? (channel.fee_rate | number) : '-' }} ppm {{ channel.fee_rate !== null ? '(' + (channel.fee_rate / 10000 | number) + '%)' : '' }} + +
Base fee + + + + {{ channel.base_fee_mtokens | amountShortener : 0 }} + msats + + + - + + + + {{ channel.base_fee_mtokens === 0 ? 'Zero base fee' : 'Non-zero base fee' }} + + +
Min HTLC + +
Max HTLC + +
Timelock delta + +
{{ i }} blocks diff --git a/frontend/src/app/lightning/channel/channel-box/channel-box.component.scss b/frontend/src/app/lightning/channel/channel-box/channel-box.component.scss index bc7b56f62..f157f380a 100644 --- a/frontend/src/app/lightning/channel/channel-box/channel-box.component.scss +++ b/frontend/src/app/lightning/channel/channel-box/channel-box.component.scss @@ -21,4 +21,10 @@ .box { margin-bottom: 20px; } +} + +.base-fee { + @media (max-width: 768px) { + padding-right: 0px; + } } \ No newline at end of file diff --git a/frontend/src/app/lightning/channel/channel-preview.component.ts b/frontend/src/app/lightning/channel/channel-preview.component.ts index 10c57c085..9f1bea4b8 100644 --- a/frontend/src/app/lightning/channel/channel-preview.component.ts +++ b/frontend/src/app/lightning/channel/channel-preview.component.ts @@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { Observable, of } from 'rxjs'; import { catchError, switchMap, tap } from 'rxjs/operators'; -import { SeoService } from 'src/app/services/seo.service'; -import { OpenGraphService } from 'src/app/services/opengraph.service'; +import { SeoService } from '../../services/seo.service'; +import { OpenGraphService } from '../../services/opengraph.service'; import { LightningApiService } from '../lightning-api.service'; @Component({ diff --git a/frontend/src/app/lightning/channel/channel.component.ts b/frontend/src/app/lightning/channel/channel.component.ts index c6d06683a..a32414449 100644 --- a/frontend/src/app/lightning/channel/channel.component.ts +++ b/frontend/src/app/lightning/channel/channel.component.ts @@ -2,9 +2,9 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { Observable, of, zip } from 'rxjs'; import { catchError, map, shareReplay, switchMap, tap } from 'rxjs/operators'; -import { IChannel } from 'src/app/interfaces/node-api.interface'; -import { ElectrsApiService } from 'src/app/services/electrs-api.service'; -import { SeoService } from 'src/app/services/seo.service'; +import { IChannel } from '../../interfaces/node-api.interface'; +import { ElectrsApiService } from '../../services/electrs-api.service'; +import { SeoService } from '../../services/seo.service'; import { LightningApiService } from '../lightning-api.service'; @Component({ diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.ts b/frontend/src/app/lightning/channels-list/channels-list.component.ts index 75b8263e2..230202d76 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.ts +++ b/frontend/src/app/lightning/channels-list/channels-list.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnI import { FormBuilder, FormGroup } from '@angular/forms'; import { BehaviorSubject, merge, Observable } from 'rxjs'; import { map, switchMap, tap } from 'rxjs/operators'; -import { isMobile } from 'src/app/shared/common.utils'; +import { isMobile } from '../../shared/common.utils'; import { LightningApiService } from '../lightning-api.service'; @Component({ diff --git a/frontend/src/app/lightning/group/group.component.html b/frontend/src/app/lightning/group/group.component.html new file mode 100644 index 000000000..fdc79b692 --- /dev/null +++ b/frontend/src/app/lightning/group/group.component.html @@ -0,0 +1,129 @@ +
+
Lightning node group
+ +
+
+ +
+

The Mempool Open Source Project

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + +
Description
These are the Lightning nodes operated by The Mempool Open Source Project that provide data for the mempool.space website. Connect to us! +
+
Nodes{{ nodes.nodes.length }}
Liquidity + + + {{ nodes.sumLiquidity | amountShortener: 1 }} + sats + +   + + +
Channels{{ nodes.sumChannels }}
+
+
+
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
AliasConnectLocation
+
+ {{ node.alias }} +
{{ node.opened_channel_count }} channel(s), + + {{ node.capacity | amountShortener: 1 }} sats + +
+
+
+
+ + {{ node.socketsObject[selectedSocketIndex].label }} + + + + +
+
+ +
+ + + + + +
+
+ +
diff --git a/frontend/src/app/lightning/group/group.component.scss b/frontend/src/app/lightning/group/group.component.scss new file mode 100644 index 000000000..fedf2d78e --- /dev/null +++ b/frontend/src/app/lightning/group/group.component.scss @@ -0,0 +1,59 @@ +.logo-container { + width: 50px; +} + +.header { + text-align: center; + display: flex; +} + +h1 { + margin-left: 15px; +} + +.qr-wrapper { + background-color: #FFF; + padding: 10px; + padding-bottom: 5px; + display: inline-block; + + + position: absolute; + bottom: 50px; + left: -175px; + z-index: 100; +} + +.dropdownLabel { + min-width: 50px; + display: inline-block; +} + +#inputGroupFileAddon04 { + position: relative; +} + +.toggle-holder { + display: flex; + width: 100%; + justify-content: flex-end; +} +@media (max-width: 767.98px) { + .text-truncate { + width: 120px; + } + .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; + } +} + +.second-line { + font-size: 12px; +} + +.description-text { + white-space: break-spaces; +} diff --git a/frontend/src/app/lightning/group/group.component.ts b/frontend/src/app/lightning/group/group.component.ts new file mode 100644 index 000000000..0517bea32 --- /dev/null +++ b/frontend/src/app/lightning/group/group.component.ts @@ -0,0 +1,103 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { map, Observable, share } from 'rxjs'; +import { SeoService } from '../../services/seo.service'; +import { GeolocationData } from '../../shared/components/geolocation/geolocation.component'; +import { LightningApiService } from '../lightning-api.service'; + +@Component({ + selector: 'app-group', + templateUrl: './group.component.html', + styleUrls: ['./group.component.scss'] +}) +export class GroupComponent implements OnInit { + nodes$: Observable; + isp: {name: string, id: number}; + + skeletonLines: number[] = []; + selectedSocketIndex = 0; + qrCodeVisible = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + socketToggleForm: FormGroup; + + constructor( + private lightningApiService: LightningApiService, + private seoService: SeoService, + private formBuilder: FormBuilder, + ) { + for (let i = 0; i < 20; ++i) { + this.skeletonLines.push(i); + } + } + + ngOnInit(): void { + this.socketToggleForm = this.formBuilder.group({ + socket: [this.selectedSocketIndex], + }); + + this.socketToggleForm.get('socket').valueChanges.subscribe((val) => { + this.selectedSocketIndex = val; + }); + + this.seoService.setTitle(`Mempool.space Lightning Nodes`); + + this.nodes$ = this.lightningApiService.getNodGroupNodes$('mempool.space') + .pipe( + map((nodes) => { + for (const node of nodes) { + const socketsObject = []; + for (const socket of node.sockets.split(',')) { + if (socket === '') { + continue; + } + let label = ''; + if (socket.match(/(?:[0-9]{1,3}\.){3}[0-9]{1,3}/)) { + label = 'IPv4'; + } else if (socket.indexOf('[') > -1) { + label = 'IPv6'; + } else if (socket.indexOf('onion') > -1) { + label = 'Tor'; + } + socketsObject.push({ + label: label, + socket: node.public_key + '@' + socket, + }); + } + // @ts-ignore + node.socketsObject = socketsObject; + + if (!node?.country && !node?.city && + !node?.subdivision) { + // @ts-ignore + node.geolocation = null; + } else { + // @ts-ignore + node.geolocation = { + country: node.country?.en, + city: node.city?.en, + subdivision: node.subdivision?.en, + iso: node.iso_code, + }; + } + } + const sumLiquidity = nodes.reduce((partialSum, a) => partialSum + parseInt(a.capacity, 10), 0); + const sumChannels = nodes.reduce((partialSum, a) => partialSum + a.opened_channel_count, 0); + + return { + nodes: nodes, + sumLiquidity: sumLiquidity, + sumChannels: sumChannels, + }; + }), + share() + ); + } + + trackByPublicKey(index: number, node: any): string { + return node.public_key; + } + + changeSocket(index: number) { + this.selectedSocketIndex = index; + } + +} diff --git a/frontend/src/app/lightning/lightning-api.service.ts b/frontend/src/app/lightning/lightning-api.service.ts index cae853df5..7a38538ff 100644 --- a/frontend/src/app/lightning/lightning-api.service.ts +++ b/frontend/src/app/lightning/lightning-api.service.ts @@ -27,6 +27,10 @@ export class LightningApiService { return this.httpClient.get(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey); } + getNodGroupNodes$(name: string): Observable { + return this.httpClient.get(this.apiBasePath + '/api/v1/lightning/nodes/group/' + name); + } + getChannel$(shortId: string): Observable { return this.httpClient.get(this.apiBasePath + '/api/v1/lightning/channels/' + shortId); } diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html index f6f083f41..f7f505b5c 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html @@ -84,3 +84,10 @@
+ + + + +
diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts index 063e2c6a5..b14d65ae0 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts @@ -1,8 +1,9 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { share } from 'rxjs/operators'; -import { INodesRanking } from 'src/app/interfaces/node-api.interface'; -import { SeoService } from 'src/app/services/seo.service'; +import { INodesRanking } from '../../interfaces/node-api.interface'; +import { SeoService } from '../../services/seo.service'; +import { StateService } from '../../services/state.service'; import { LightningApiService } from '../lightning-api.service'; @Component({ @@ -14,10 +15,12 @@ import { LightningApiService } from '../lightning-api.service'; export class LightningDashboardComponent implements OnInit { statistics$: Observable; nodesRanking$: Observable; + officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; constructor( private lightningApiService: LightningApiService, private seoService: SeoService, + private stateService: StateService, ) { } ngOnInit(): void { diff --git a/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.ts b/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.ts index c38a99fde..11bf4dc5d 100644 --- a/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.ts +++ b/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-lightning-wrapper', diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts index beb0b5c46..3c06fb023 100644 --- a/frontend/src/app/lightning/lightning.module.ts +++ b/frontend/src/app/lightning/lightning.module.ts @@ -30,6 +30,7 @@ import { TopNodesPerCapacity } from '../lightning/nodes-ranking/top-nodes-per-ca import { OldestNodes } from '../lightning/nodes-ranking/oldest-nodes/oldest-nodes.component'; import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component'; import { NodeChannels } from '../lightning/nodes-channels/node-channels.component'; +import { GroupComponent } from './group/group.component'; @NgModule({ declarations: [ @@ -58,6 +59,7 @@ import { NodeChannels } from '../lightning/nodes-channels/node-channels.componen OldestNodes, NodesRankingsDashboard, NodeChannels, + GroupComponent, ], imports: [ CommonModule, diff --git a/frontend/src/app/lightning/lightning.routing.module.ts b/frontend/src/app/lightning/lightning.routing.module.ts index 173bae237..e2121f8e8 100644 --- a/frontend/src/app/lightning/lightning.routing.module.ts +++ b/frontend/src/app/lightning/lightning.routing.module.ts @@ -8,6 +8,7 @@ import { NodesPerCountry } from './nodes-per-country/nodes-per-country.component import { NodesPerISP } from './nodes-per-isp/nodes-per-isp.component'; import { NodesRanking } from './nodes-ranking/nodes-ranking.component'; import { NodesRankingsDashboard } from './nodes-rankings-dashboard/nodes-rankings-dashboard.component'; +import { GroupComponent } from './group/group.component'; const routes: Routes = [ { @@ -34,6 +35,10 @@ const routes: Routes = [ path: 'nodes/isp/:isp', component: NodesPerISP, }, + { + path: 'group/the-mempool-open-source-project', + component: GroupComponent, + }, { path: 'nodes/rankings', component: NodesRankingsDashboard, diff --git a/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts index 6f0721d38..d4aca013c 100644 --- a/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts +++ b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts @@ -4,8 +4,8 @@ import { Observable } from 'rxjs'; import { switchMap, tap } from 'rxjs/operators'; import { formatNumber } from '@angular/common'; import { FormGroup } from '@angular/forms'; -import { StorageService } from 'src/app/services/storage.service'; -import { download } from 'src/app/shared/graphs.utils'; +import { StorageService } from '../../services/storage.service'; +import { download } from '../../shared/graphs.utils'; import { LightningApiService } from '../lightning-api.service'; import { ActivatedRoute, ParamMap } from '@angular/router'; diff --git a/frontend/src/app/lightning/node/node-preview.component.ts b/frontend/src/app/lightning/node/node-preview.component.ts index 574ee51de..4c2782c2d 100644 --- a/frontend/src/app/lightning/node/node-preview.component.ts +++ b/frontend/src/app/lightning/node/node-preview.component.ts @@ -2,9 +2,9 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { Observable } from 'rxjs'; import { catchError, map, switchMap } from 'rxjs/operators'; -import { SeoService } from 'src/app/services/seo.service'; -import { OpenGraphService } from 'src/app/services/opengraph.service'; -import { getFlagEmoji } from 'src/app/shared/common.utils'; +import { SeoService } from '../../services/seo.service'; +import { OpenGraphService } from '../../services/opengraph.service'; +import { getFlagEmoji } from '../../shared/common.utils'; import { LightningApiService } from '../lightning-api.service'; import { isMobile } from '../../shared/common.utils'; diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index 0c8451d44..92f731bef 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -85,9 +85,12 @@ {{ node.as_organization }} [ASN {{node.as_number}}] - + Exclusively on Tor + + Unknown + @@ -120,9 +123,7 @@ - +
@@ -230,9 +231,7 @@ - +

diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index aad8e14d0..cbfa66c89 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -2,9 +2,9 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { Observable } from 'rxjs'; import { catchError, map, switchMap } from 'rxjs/operators'; -import { SeoService } from 'src/app/services/seo.service'; +import { SeoService } from '../../services/seo.service'; import { LightningApiService } from '../lightning-api.service'; -import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component'; +import { GeolocationData } from '../../shared/components/geolocation/geolocation.component'; @Component({ selector: 'app-node', @@ -22,6 +22,8 @@ export class NodeComponent implements OnInit { error: Error; publicKey: string; channelListLoading = false; + clearnetSocketCount = 0; + torSocketCount = 0; constructor( private lightningApiService: LightningApiService, @@ -47,10 +49,13 @@ export class NodeComponent implements OnInit { let label = ''; if (socket.match(/(?:[0-9]{1,3}\.){3}[0-9]{1,3}/)) { label = 'IPv4'; + this.clearnetSocketCount++; } else if (socket.indexOf('[') > -1) { label = 'IPv6'; + this.clearnetSocketCount++; } else if (socket.indexOf('onion') > -1) { label = 'Tor'; + this.torSocketCount++; } socketsObject.push({ label: label, diff --git a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts index 0a20d6e79..22b846458 100644 --- a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts +++ b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts @@ -1,14 +1,14 @@ import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter, NgZone, OnInit } from '@angular/core'; -import { SeoService } from 'src/app/services/seo.service'; -import { ApiService } from 'src/app/services/api.service'; +import { SeoService } from '../../services/seo.service'; +import { ApiService } from '../../services/api.service'; import { Observable, switchMap, tap, zip } from 'rxjs'; -import { AssetsService } from 'src/app/services/assets.service'; +import { AssetsService } from '../../services/assets.service'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; -import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; -import { StateService } from 'src/app/services/state.service'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; +import { StateService } from '../../services/state.service'; import { EChartsOption, registerMap } from 'echarts'; import 'echarts-gl'; -import { isMobile } from 'src/app/shared/common.utils'; +import { isMobile } from '../../shared/common.utils'; @Component({ selector: 'app-nodes-channels-map', diff --git a/frontend/src/app/lightning/nodes-channels/node-channels.component.ts b/frontend/src/app/lightning/nodes-channels/node-channels.component.ts index f675c81b5..91d43f6ab 100644 --- a/frontend/src/app/lightning/nodes-channels/node-channels.component.ts +++ b/frontend/src/app/lightning/nodes-channels/node-channels.component.ts @@ -3,10 +3,10 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, O import { Router } from '@angular/router'; import { ECharts, EChartsOption, TreemapSeriesOption } from 'echarts'; import { Observable, share, switchMap, tap } from 'rxjs'; -import { lerpColor } from 'src/app/shared/graphs.utils'; -import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe'; +import { lerpColor } from '../../shared/graphs.utils'; +import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; import { LightningApiService } from '../lightning-api.service'; -import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { StateService } from '../../services/state.service'; @Component({ diff --git a/frontend/src/app/lightning/nodes-map/nodes-map.component.ts b/frontend/src/app/lightning/nodes-map/nodes-map.component.ts index 8ec853aaa..5751c65f1 100644 --- a/frontend/src/app/lightning/nodes-map/nodes-map.component.ts +++ b/frontend/src/app/lightning/nodes-map/nodes-map.component.ts @@ -1,15 +1,15 @@ import { ChangeDetectionStrategy, Component, Inject, Input, Output, EventEmitter, LOCALE_ID, NgZone, OnDestroy, OnInit, OnChanges } from '@angular/core'; -import { SeoService } from 'src/app/services/seo.service'; -import { ApiService } from 'src/app/services/api.service'; +import { SeoService } from '../../services/seo.service'; +import { ApiService } from '../../services/api.service'; import { Observable, BehaviorSubject, switchMap, tap, combineLatest } from 'rxjs'; -import { AssetsService } from 'src/app/services/assets.service'; +import { AssetsService } from '../../services/assets.service'; import { EChartsOption, registerMap } from 'echarts'; -import { lerpColor } from 'src/app/shared/graphs.utils'; +import { lerpColor } from '../../shared/graphs.utils'; import { Router } from '@angular/router'; -import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; -import { StateService } from 'src/app/services/state.service'; -import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe'; -import { getFlagEmoji } from 'src/app/shared/common.utils'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; +import { StateService } from '../../services/state.service'; +import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; +import { getFlagEmoji } from '../../shared/common.utils'; @Component({ selector: 'app-nodes-map', @@ -46,7 +46,9 @@ export class NodesMap implements OnInit, OnChanges { } ngOnInit(): void { - this.seoService.setTitle($localize`Lightning nodes world map`); + if (!this.widget) { + this.seoService.setTitle($localize`Lightning nodes world map`); + } if (!this.inputNodes$) { this.inputNodes$ = new BehaviorSubject(this.nodes); diff --git a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts index 0ef9e3cd5..c1647cd25 100644 --- a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts +++ b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts @@ -1,16 +1,16 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core'; -import { EChartsOption, graphic} from 'echarts'; +import { EChartsOption, graphic, LineSeriesOption} from 'echarts'; import { Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; import { formatNumber } from '@angular/common'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { StorageService } from 'src/app/services/storage.service'; -import { MiningService } from 'src/app/services/mining.service'; -import { download } from 'src/app/shared/graphs.utils'; -import { SeoService } from 'src/app/services/seo.service'; +import { StorageService } from '../../services/storage.service'; +import { MiningService } from '../../services/mining.service'; +import { download } from '../../shared/graphs.utils'; +import { SeoService } from '../../services/seo.service'; import { LightningApiService } from '../lightning-api.service'; -import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe'; -import { isMobile } from 'src/app/shared/common.utils'; +import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; +import { isMobile } from '../../shared/common.utils'; @Component({ selector: 'app-nodes-networks-chart', @@ -89,10 +89,11 @@ export class NodesNetworksChartComponent implements OnInit { tor_nodes: data.map(val => [val.added * 1000, val.tor_nodes]), clearnet_nodes: data.map(val => [val.added * 1000, val.clearnet_nodes]), unannounced_nodes: data.map(val => [val.added * 1000, val.unannounced_nodes]), + clearnet_tor_nodes: data.map(val => [val.added * 1000, val.clearnet_tor_nodes]), }; let maxYAxis = 0; for (const day of data) { - maxYAxis = Math.max(maxYAxis, day.tor_nodes + day.clearnet_nodes + day.unannounced_nodes); + maxYAxis = Math.max(maxYAxis, day.tor_nodes + day.clearnet_nodes + day.unannounced_nodes + day.clearnet_tor_nodes); } maxYAxis = Math.ceil(maxYAxis / 3000) * 3000; this.prepareChartOptions(chartData, maxYAxis); @@ -134,6 +135,94 @@ export class NodesNetworksChartComponent implements OnInit { }; } + const series: LineSeriesOption[] = [ + { + zlevel: 1, + yAxisIndex: 0, + name: $localize`Unknown`, + showSymbol: false, + symbol: 'none', + data: data.unannounced_nodes, + type: 'line', + lineStyle: { + width: 2, + }, + areaStyle: { + opacity: 0.5, + }, + stack: 'Total', + color: new graphic.LinearGradient(0, 0.75, 0, 1, [ + { offset: 0, color: '#D81B60' }, + { offset: 1, color: '#D81B60AA' }, + ]), + + smooth: false, + }, + { + zlevel: 1, + yAxisIndex: 0, + name: $localize`Reachable on Clearnet Only`, + showSymbol: false, + symbol: 'none', + data: data.clearnet_nodes, + type: 'line', + lineStyle: { + width: 2, + }, + areaStyle: { + opacity: 0.5, + }, + stack: 'Total', + color: new graphic.LinearGradient(0, 0.75, 0, 1, [ + { offset: 0, color: '#FFB300' }, + { offset: 1, color: '#FFB300AA' }, + ]), + smooth: false, + }, + { + zlevel: 1, + yAxisIndex: 0, + name: $localize`Reachable on Clearnet and Darknet`, + showSymbol: false, + symbol: 'none', + data: data.clearnet_tor_nodes, + type: 'line', + lineStyle: { + width: 2, + }, + areaStyle: { + opacity: 0.5, + }, + stack: 'Total', + color: new graphic.LinearGradient(0, 0.75, 0, 1, [ + { offset: 0, color: '#be7d4c' }, + { offset: 1, color: '#be7d4cAA' }, + ]), + smooth: false, + }, + { + zlevel: 1, + yAxisIndex: 0, + name: $localize`Reachable on Darknet Only`, + showSymbol: false, + symbol: 'none', + data: data.tor_nodes, + type: 'line', + lineStyle: { + width: 2, + }, + areaStyle: { + opacity: 0.5, + }, + stack: 'Total', + color: new graphic.LinearGradient(0, 0.75, 0, 1, [ + { offset: 0, color: '#7D4698' }, + { offset: 1, color: '#7D4698AA' }, + ]), + smooth: false, + }, + ]; + this.chartOptions = { title: title, animation: false, @@ -164,12 +253,17 @@ export class NodesNetworksChartComponent implements OnInit { let tooltip = `${date}
`; for (const tick of ticks.reverse()) { + if (tick.seriesName.indexOf('ignored') !== -1) { + continue; + } if (tick.seriesIndex === 0) { // Tor tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`; } else if (tick.seriesIndex === 1) { // Clearnet tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`; } else if (tick.seriesIndex === 2) { // Unannounced tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`; + } else if (tick.seriesIndex === 3) { // Tor + Clearnet + tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`; } tooltip += `
`; total += tick.data[1]; @@ -190,7 +284,7 @@ export class NodesNetworksChartComponent implements OnInit { padding: 10, data: [ { - name: $localize`Total`, + name: $localize`Reachable on Darknet Only`, inactiveColor: 'rgb(110, 112, 121)', textStyle: { color: 'white', @@ -198,7 +292,7 @@ export class NodesNetworksChartComponent implements OnInit { icon: 'roundRect', }, { - name: $localize`Tor`, + name: $localize`Reachable on Clearnet and Darknet`, inactiveColor: 'rgb(110, 112, 121)', textStyle: { color: 'white', @@ -206,7 +300,7 @@ export class NodesNetworksChartComponent implements OnInit { icon: 'roundRect', }, { - name: $localize`Clearnet`, + name: $localize`Reachable on Clearnet Only`, inactiveColor: 'rgb(110, 112, 121)', textStyle: { color: 'white', @@ -214,7 +308,7 @@ export class NodesNetworksChartComponent implements OnInit { icon: 'roundRect', }, { - name: $localize`Unannounced`, + name: $localize`Unknown`, inactiveColor: 'rgb(110, 112, 121)', textStyle: { color: 'white', @@ -223,10 +317,10 @@ export class NodesNetworksChartComponent implements OnInit { }, ], selected: this.widget ? undefined : JSON.parse(this.storageService.getValue('nodes_networks_legend')) ?? { - 'Total': true, - 'Tor': true, - 'Clearnet': true, - 'Unannounced': true, + '$localize`Reachable on Darknet Only`': true, + '$localize`Reachable on Clearnet Only`': true, + '$localize`Reachable on Clearnet and Darknet`': true, + '$localize`Unknown`': true, } }, yAxis: data.tor_nodes.length === 0 ? undefined : [ @@ -250,7 +344,6 @@ export class NodesNetworksChartComponent implements OnInit { opacity: 0.25, }, }, - max: maxYAxis, min: 0, interval: 3000, }, @@ -274,77 +367,25 @@ export class NodesNetworksChartComponent implements OnInit { opacity: 0.25, }, }, - max: maxYAxis, min: 0, interval: 3000, } ], - series: data.tor_nodes.length === 0 ? [] : [ - { - zlevel: 1, - yAxisIndex: 0, - name: $localize`Unannounced`, - showSymbol: false, - symbol: 'none', - data: data.unannounced_nodes, - type: 'line', - lineStyle: { - width: 2, - }, - areaStyle: { - opacity: 0.5, - }, - stack: 'Total', - color: new graphic.LinearGradient(0, 0.75, 0, 1, [ - { offset: 0, color: '#D81B60' }, - { offset: 1, color: '#D81B60AA' }, - ]), - - smooth: false, - }, - { - zlevel: 1, - yAxisIndex: 0, - name: $localize`Clearnet`, - showSymbol: false, - symbol: 'none', - data: data.clearnet_nodes, - type: 'line', - lineStyle: { - width: 2, - }, - areaStyle: { - opacity: 0.5, - }, - stack: 'Total', - color: new graphic.LinearGradient(0, 0.75, 0, 1, [ - { offset: 0, color: '#FFB300' }, - { offset: 1, color: '#FFB300AA' }, - ]), - smooth: false, - }, - { - zlevel: 1, - yAxisIndex: 1, - name: $localize`Tor`, - showSymbol: false, - symbol: 'none', - data: data.tor_nodes, - type: 'line', - lineStyle: { - width: 2, - }, - areaStyle: { - opacity: 0.5, - }, - stack: 'Total', - color: new graphic.LinearGradient(0, 0.75, 0, 1, [ - { offset: 0, color: '#7D4698' }, - { offset: 1, color: '#7D4698AA' }, - ]), - smooth: false, - }, - ], + series: data.tor_nodes.length === 0 ? [] : series.concat(series.map((serie) => { + // We create dummy duplicated series so when we use the data zoom, the y axis + // both scales properly + const invisibleSerie = {...serie}; + invisibleSerie.name = 'ignored' + Math.random().toString(); + invisibleSerie.stack = 'ignored'; + invisibleSerie.yAxisIndex = 1; + invisibleSerie.lineStyle = { + opacity: 0, + }; + invisibleSerie.areaStyle = { + opacity: 0, + }; + return invisibleSerie; + })), dataZoom: this.widget ? null : [{ type: 'inside', realtime: true, @@ -371,6 +412,11 @@ export class NodesNetworksChartComponent implements OnInit { }, }], }; + + if (isMobile()) { + // @ts-ignore + this.chartOptions.legend.left = 50; + } } onChartInit(ec): void { diff --git a/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts b/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts index 9f1b3fe88..bf4a660c1 100644 --- a/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts +++ b/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts @@ -2,14 +2,14 @@ import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone } from import { Router } from '@angular/router'; import { EChartsOption, PieSeriesOption } from 'echarts'; import { map, Observable, share, tap } from 'rxjs'; -import { chartColors } from 'src/app/app.constants'; -import { ApiService } from 'src/app/services/api.service'; -import { SeoService } from 'src/app/services/seo.service'; -import { StateService } from 'src/app/services/state.service'; -import { download } from 'src/app/shared/graphs.utils'; -import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe'; -import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; -import { getFlagEmoji } from 'src/app/shared/common.utils'; +import { chartColors } from '../../app.constants'; +import { ApiService } from '../../services/api.service'; +import { SeoService } from '../../services/seo.service'; +import { StateService } from '../../services/state.service'; +import { download } from '../../shared/graphs.utils'; +import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; +import { getFlagEmoji } from '../../shared/common.utils'; @Component({ selector: 'app-nodes-per-country-chart', diff --git a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts index 19394a828..01eb6d1cf 100644 --- a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts +++ b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts @@ -1,10 +1,10 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { map, Observable, share } from 'rxjs'; -import { ApiService } from 'src/app/services/api.service'; -import { SeoService } from 'src/app/services/seo.service'; -import { getFlagEmoji } from 'src/app/shared/common.utils'; -import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component'; +import { ApiService } from '../../services/api.service'; +import { SeoService } from '../../services/seo.service'; +import { getFlagEmoji } from '../../shared/common.utils'; +import { GeolocationData } from '../../shared/components/geolocation/geolocation.component'; @Component({ selector: 'app-nodes-per-country', diff --git a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts index 635e5bc74..5150f84a7 100644 --- a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts +++ b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts @@ -2,14 +2,14 @@ import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone, Input import { Router } from '@angular/router'; import { EChartsOption, PieSeriesOption } from 'echarts'; import { combineLatest, map, Observable, share, startWith, Subject, switchMap, tap } from 'rxjs'; -import { chartColors } from 'src/app/app.constants'; -import { ApiService } from 'src/app/services/api.service'; -import { SeoService } from 'src/app/services/seo.service'; -import { StateService } from 'src/app/services/state.service'; -import { isMobile } from 'src/app/shared/common.utils'; -import { download } from 'src/app/shared/graphs.utils'; -import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe'; -import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; +import { chartColors } from '../../app.constants'; +import { ApiService } from '../../services/api.service'; +import { SeoService } from '../../services/seo.service'; +import { StateService } from '../../services/state.service'; +import { isMobile } from '../../shared/common.utils'; +import { download } from '../../shared/graphs.utils'; +import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; @Component({ selector: 'app-nodes-per-isp-chart', diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts index 18e2f2d6c..759606372 100644 --- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts +++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts @@ -1,11 +1,11 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { catchError, map, switchMap, Observable, share, of } from 'rxjs'; -import { ApiService } from 'src/app/services/api.service'; -import { SeoService } from 'src/app/services/seo.service'; -import { OpenGraphService } from 'src/app/services/opengraph.service'; -import { getFlagEmoji } from 'src/app/shared/common.utils'; -import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component'; +import { ApiService } from '../../services/api.service'; +import { SeoService } from '../../services/seo.service'; +import { OpenGraphService } from '../../services/opengraph.service'; +import { getFlagEmoji } from '../../shared/common.utils'; +import { GeolocationData } from '../../shared/components/geolocation/geolocation.component'; @Component({ selector: 'app-nodes-per-isp-preview', diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts index 24664aab0..e87482583 100644 --- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts +++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts @@ -1,10 +1,10 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { map, Observable, share } from 'rxjs'; -import { ApiService } from 'src/app/services/api.service'; -import { SeoService } from 'src/app/services/seo.service'; -import { getFlagEmoji } from 'src/app/shared/common.utils'; -import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component'; +import { ApiService } from '../../services/api.service'; +import { SeoService } from '../../services/seo.service'; +import { getFlagEmoji } from '../../shared/common.utils'; +import { GeolocationData } from '../../shared/components/geolocation/geolocation.component'; @Component({ selector: 'app-nodes-per-isp', diff --git a/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts index d1cec0780..6ee9ed231 100644 --- a/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { map, Observable } from 'rxjs'; -import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component'; -import { SeoService } from 'src/app/services/seo.service'; +import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component'; +import { SeoService } from '../../../services/seo.service'; import { IOldestNodes } from '../../../interfaces/node-api.interface'; import { LightningApiService } from '../../lightning-api.service'; diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts index ad396ee31..bdfd22e1f 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { map, Observable } from 'rxjs'; -import { INodesRanking, ITopNodesPerCapacity } from 'src/app/interfaces/node-api.interface'; -import { SeoService } from 'src/app/services/seo.service'; -import { isMobile } from 'src/app/shared/common.utils'; -import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component'; +import { INodesRanking, ITopNodesPerCapacity } from '../../../interfaces/node-api.interface'; +import { SeoService } from '../../../services/seo.service'; +import { isMobile } from '../../../shared/common.utils'; +import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component'; import { LightningApiService } from '../../lightning-api.service'; @Component({ diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts index f8a2ae52b..719a69663 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { map, Observable } from 'rxjs'; -import { INodesRanking, ITopNodesPerChannels } from 'src/app/interfaces/node-api.interface'; -import { SeoService } from 'src/app/services/seo.service'; -import { isMobile } from 'src/app/shared/common.utils'; -import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component'; +import { INodesRanking, ITopNodesPerChannels } from '../../../interfaces/node-api.interface'; +import { SeoService } from '../../../services/seo.service'; +import { isMobile } from '../../../shared/common.utils'; +import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component'; import { LightningApiService } from '../../lightning-api.service'; @Component({ diff --git a/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts b/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts index 4b39d8467..90342c557 100644 --- a/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts +++ b/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { Observable, share } from 'rxjs'; -import { INodesRanking } from 'src/app/interfaces/node-api.interface'; -import { SeoService } from 'src/app/services/seo.service'; +import { INodesRanking } from '../../interfaces/node-api.interface'; +import { SeoService } from '../../services/seo.service'; import { LightningApiService } from '../lightning-api.service'; @Component({ diff --git a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts index 3a25367dc..e4b71ae41 100644 --- a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts +++ b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts @@ -2,15 +2,15 @@ import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angul import { EChartsOption, graphic } from 'echarts'; import { Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { SeoService } from 'src/app/services/seo.service'; +import { SeoService } from '../../services/seo.service'; import { formatNumber } from '@angular/common'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { StorageService } from 'src/app/services/storage.service'; -import { MiningService } from 'src/app/services/mining.service'; -import { download } from 'src/app/shared/graphs.utils'; +import { StorageService } from '../../services/storage.service'; +import { MiningService } from '../../services/mining.service'; +import { download } from '../../shared/graphs.utils'; import { LightningApiService } from '../lightning-api.service'; -import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe'; -import { isMobile } from 'src/app/shared/common.utils'; +import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; +import { isMobile } from '../../shared/common.utils'; @Component({ selector: 'app-lightning-statistics-chart', diff --git a/frontend/src/app/previews.module.ts b/frontend/src/app/previews.module.ts index 166670ced..2e8dbdc75 100644 --- a/frontend/src/app/previews.module.ts +++ b/frontend/src/app/previews.module.ts @@ -7,20 +7,22 @@ import { PreviewsRoutingModule } from './previews.routing.module'; import { TransactionPreviewComponent } from './components/transaction/transaction-preview.component'; import { BlockPreviewComponent } from './components/block/block-preview.component'; import { AddressPreviewComponent } from './components/address/address-preview.component'; +import { PoolPreviewComponent } from './components/pool/pool-preview.component'; import { MasterPagePreviewComponent } from './components/master-page-preview/master-page-preview.component'; @NgModule({ declarations: [ TransactionPreviewComponent, BlockPreviewComponent, AddressPreviewComponent, + PoolPreviewComponent, MasterPagePreviewComponent, ], imports: [ CommonModule, SharedModule, RouterModule, - GraphsModule, PreviewsRoutingModule, + GraphsModule, ], }) export class PreviewsModule { } diff --git a/frontend/src/app/previews.routing.module.ts b/frontend/src/app/previews.routing.module.ts index 5ac13c36d..c2ad8db5f 100644 --- a/frontend/src/app/previews.routing.module.ts +++ b/frontend/src/app/previews.routing.module.ts @@ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router'; import { TransactionPreviewComponent } from './components/transaction/transaction-preview.component'; import { BlockPreviewComponent } from './components/block/block-preview.component'; import { AddressPreviewComponent } from './components/address/address-preview.component'; +import { PoolPreviewComponent } from './components/pool/pool-preview.component'; import { MasterPagePreviewComponent } from './components/master-page-preview/master-page-preview.component'; const routes: Routes = [ @@ -24,6 +25,10 @@ const routes: Routes = [ children: [], component: TransactionPreviewComponent }, + { + path: 'mining/pool/:slug', + component: PoolPreviewComponent + }, { path: 'lightning', loadChildren: () => import('./lightning/lightning-previews.module').then(m => m.LightningPreviewsModule) diff --git a/frontend/src/app/services/language.service.ts b/frontend/src/app/services/language.service.ts index 370028015..58ba94a00 100644 --- a/frontend/src/app/services/language.service.ts +++ b/frontend/src/app/services/language.service.ts @@ -1,6 +1,6 @@ import { DOCUMENT, getLocaleId } from '@angular/common'; import { LOCALE_ID, Inject, Injectable } from '@angular/core'; -import { languages } from 'src/app/app.constants'; +import { languages } from '../app.constants'; @Injectable({ providedIn: 'root' diff --git a/frontend/src/app/shared/components/geolocation/geolocation.component.ts b/frontend/src/app/shared/components/geolocation/geolocation.component.ts index afcea3b1e..9cce1ea08 100644 --- a/frontend/src/app/shared/components/geolocation/geolocation.component.ts +++ b/frontend/src/app/shared/components/geolocation/geolocation.component.ts @@ -31,25 +31,34 @@ export class GeolocationComponent implements OnChanges { } if (this.type === 'list-country') { - if (this.data.city) { + if (!this.data.city) { + this.formattedLocation = '-'; + } + else if (this.data.city) { this.formattedLocation += ' ' + city; if (this.data.subdivision) { this.formattedLocation += ', ' + subdivision; } - } else { - this.formattedLocation += '-'; } } if (this.type === 'list-isp') { - this.formattedLocation = getFlagEmoji(this.data.iso); - if (this.data.city) { - this.formattedLocation += ' ' + city; - if (this.data.subdivision) { - this.formattedLocation += ', ' + subdivision; - } + if (!this.data.country && !this.data.city) { + this.formattedLocation = '-'; } else { - this.formattedLocation += ' ' + this.data.country; + if (this.data.country) { + this.formattedLocation = getFlagEmoji(this.data.iso); + } else { + this.formattedLocation = ''; + } + if (this.data.city) { + this.formattedLocation += ' ' + city; + if (this.data.subdivision) { + this.formattedLocation += ', ' + subdivision; + } + } else { + this.formattedLocation += ' ' + this.data.country; + } } } diff --git a/frontend/src/app/shared/pipes/amount-shortener.pipe.ts b/frontend/src/app/shared/pipes/amount-shortener.pipe.ts index db3d94284..71ff76f77 100644 --- a/frontend/src/app/shared/pipes/amount-shortener.pipe.ts +++ b/frontend/src/app/shared/pipes/amount-shortener.pipe.ts @@ -7,6 +7,7 @@ export class AmountShortenerPipe implements PipeTransform { transform(num: number, ...args: any[]): unknown { const digits = args[0] ?? 1; const unit = args[1] || undefined; + const isMoney = args[2] || false; if (num < 1000) { return num.toFixed(digits); @@ -16,7 +17,7 @@ export class AmountShortenerPipe implements PipeTransform { { value: 1, symbol: '' }, { value: 1e3, symbol: 'k' }, { value: 1e6, symbol: 'M' }, - { value: 1e9, symbol: 'G' }, + { value: 1e9, symbol: isMoney ? 'B' : 'G' }, { value: 1e12, symbol: 'T' }, { value: 1e15, symbol: 'P' }, { value: 1e18, symbol: 'E' } diff --git a/frontend/src/app/shared/pipes/relative-url/relative-url.pipe.ts b/frontend/src/app/shared/pipes/relative-url/relative-url.pipe.ts index 2a6cc4d28..d7fe612fe 100644 --- a/frontend/src/app/shared/pipes/relative-url/relative-url.pipe.ts +++ b/frontend/src/app/shared/pipes/relative-url/relative-url.pipe.ts @@ -1,5 +1,5 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { StateService } from 'src/app/services/state.service'; +import { StateService } from '../../../services/state.service'; @Pipe({ name: 'relativeUrl' diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index be4ba2fe0..c4973d75c 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -61,6 +61,7 @@ import { FeesBoxComponent } from '../components/fees-box/fees-box.component'; import { DifficultyComponent } from '../components/difficulty/difficulty.component'; import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component'; import { TxBowtieGraphComponent } from '../components/tx-bowtie-graph/tx-bowtie-graph.component'; +import { TxBowtieGraphTooltipComponent } from '../components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component'; import { PrivacyPolicyComponent } from '../components/privacy-policy/privacy-policy.component'; import { TrademarkPolicyComponent } from '../components/trademark-policy/trademark-policy.component'; import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component'; @@ -134,6 +135,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati FeesBoxComponent, DifficultyComponent, TxBowtieGraphComponent, + TxBowtieGraphTooltipComponent, TermsOfServiceComponent, PrivacyPolicyComponent, TrademarkPolicyComponent, @@ -236,6 +238,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati FeesBoxComponent, DifficultyComponent, TxBowtieGraphComponent, + TxBowtieGraphTooltipComponent, TermsOfServiceComponent, PrivacyPolicyComponent, TrademarkPolicyComponent, diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 65293098d..d903c6076 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -10,7 +10,8 @@ $nav-tabs-link-active-bg: #11131f; $primary: #105fb0; $secondary: #2d3348; $success: #1a9436; -$info: #1bd8f4; +$info: #5f4ab8; +$light: #744ad1; $h5-font-size: 1.15rem !default; diff --git a/unfurler/config.sample.json b/unfurler/config.sample.json index 0e4e8d530..babd9c393 100644 --- a/unfurler/config.sample.json +++ b/unfurler/config.sample.json @@ -1,7 +1,8 @@ { "SERVER": { "HOST": "http://localhost", - "HTTP_PORT": 4201 + "HTTP_PORT": 4201, + "STDOUT_LOG_MIN_PRIORITY": "debug", }, "MEMPOOL": { "HTTP_HOST": "http://localhost", @@ -14,5 +15,12 @@ "EXEC_PATH": "/usr/local/bin/chrome", // optional "MAX_PAGE_AGE": 86400, // maximum lifetime of a page session (in seconds) "RENDER_TIMEOUT": 3000, // timeout for preview image rendering (in ms) (optional) + }, + "SYSLOG": { + "ENABLED": true, + "HOST": "127.0.0.1", + "PORT": 514, + "MIN_PRIORITY": "info", + "FACILITY": "local7" } } diff --git a/unfurler/puppeteer.config.json b/unfurler/puppeteer.config.json index b3a9b7fc4..4d525755e 100644 --- a/unfurler/puppeteer.config.json +++ b/unfurler/puppeteer.config.json @@ -1,5 +1,6 @@ { "headless": true, + "dumpio": true, "defaultViewport": { "width": 1200, "height": 600 diff --git a/unfurler/src/concurrency/ReusablePage.ts b/unfurler/src/concurrency/ReusablePage.ts index ed400d004..4b272afb0 100644 --- a/unfurler/src/concurrency/ReusablePage.ts +++ b/unfurler/src/concurrency/ReusablePage.ts @@ -1,6 +1,7 @@ import * as puppeteer from 'puppeteer'; import ConcurrencyImplementation from 'puppeteer-cluster/dist/concurrency/ConcurrencyImplementation'; import { timeoutExecute } from 'puppeteer-cluster/dist/util'; +import logger from '../logger'; import config from '../config'; const mempoolHost = config.MEMPOOL.HTTP_HOST + (config.MEMPOOL.HTTP_PORT ? ':' + config.MEMPOOL.HTTP_PORT : ''); @@ -43,13 +44,13 @@ export default class ReusablePage extends ConcurrencyImplementation { } this.repairing = true; - console.log('Starting repair'); + logger.info('Starting repair'); try { // will probably fail, but just in case the repair was not necessary await (this.browser).close(); } catch (e) { - console.log('Unable to close browser.'); + logger.warn('Unable to close browser.'); } try { @@ -65,11 +66,17 @@ export default class ReusablePage extends ConcurrencyImplementation { public async init() { this.browser = await this.puppeteer.launch(this.options); + if (this.browser != null) { + const proc = this.browser.process(); + if (proc) { + initBrowserLogging(proc); + } + } const promises = [] for (let i = 0; i < maxConcurrency; i++) { const newPage = await this.initPage(); newPage.index = this.pages.length; - console.log('initialized page ', newPage.index); + logger.info(`initialized page ${newPage.index}`); this.pages.push(newPage); } } @@ -82,15 +89,29 @@ export default class ReusablePage extends ConcurrencyImplementation { const page = await (this.browser as puppeteer.Browser).newPage() as RepairablePage; page.language = null; page.createdAt = Date.now(); - const defaultUrl = mempoolHost + '/preview/block/1'; + let defaultUrl + if (config.MEMPOOL.NETWORK !== 'bisq') { + // preload the preview module + defaultUrl = mempoolHost + '/preview/block/1'; + } else { + // no preview module implemented yet for bisq + defaultUrl = mempoolHost; + } page.on('pageerror', (err) => { page.repairRequested = true; }); - await page.goto(defaultUrl, { waitUntil: "load" }); - await Promise.race([ - page.waitForSelector('meta[property="og:preview:ready"]', { timeout: config.PUPPETEER.RENDER_TIMEOUT || 3000 }).then(() => true), - page.waitForSelector('meta[property="og:preview:fail"]', { timeout: config.PUPPETEER.RENDER_TIMEOUT || 3000 }).then(() => false) - ]) + if (config.MEMPOOL.NETWORK !== 'bisq') { + try { + await page.goto(defaultUrl, { waitUntil: "load" }); + await Promise.race([ + page.waitForSelector('meta[property="og:preview:ready"]', { timeout: config.PUPPETEER.RENDER_TIMEOUT || 3000 }).then(() => true), + page.waitForSelector('meta[property="og:preview:fail"]', { timeout: config.PUPPETEER.RENDER_TIMEOUT || 3000 }).then(() => false) + ]) + } catch (e) { + logger.err(`failed to load frontend during page initialization: ` + (e instanceof Error ? e.message : `${e}`)); + page.repairRequested = true; + } + } page.free = true; return page } @@ -98,7 +119,7 @@ export default class ReusablePage extends ConcurrencyImplementation { protected async createResources(): Promise { const page = this.pages.find(p => p.free); if (!page) { - console.log('no free pages!') + logger.err('no free pages!') throw new Error('no pages available'); } else { page.free = false; @@ -117,7 +138,7 @@ export default class ReusablePage extends ConcurrencyImplementation { try { await page.goto('about:blank', {timeout: 200}); // prevents memory leak (maybe?) } catch (e) { - console.log('unexpected page repair error'); + logger.err('unexpected page repair error'); } await page.close(); return newPage; @@ -161,3 +182,26 @@ export default class ReusablePage extends ConcurrencyImplementation { }; } } + +function initBrowserLogging(proc) { + if (proc.stderr && proc.stdout) { + proc.on('error', msg => { + logger.err('BROWSER ERROR ' + msg); + }) + proc.stderr.on('data', buf => { + const msg = String(buf); + // For some reason everything (including js console logs) is piped to stderr + // so this kludge splits logs back into their priority levels + if (msg.includes(':INFO:')) { + logger.info('BROWSER' + msg, true); + } else if (msg.includes(':WARNING:')) { + logger.warn('BROWSER' + msg, true); + } else { + logger.err('BROWSER' + msg, true); + } + }) + proc.stdout.on('data', buf => { + logger.info('BROWSER' + String(buf)); + }) + } +} diff --git a/unfurler/src/config.ts b/unfurler/src/config.ts index c04f58007..76bc2a75f 100644 --- a/unfurler/src/config.ts +++ b/unfurler/src/config.ts @@ -4,6 +4,7 @@ interface IConfig { SERVER: { HOST: 'http://localhost'; HTTP_PORT: number; + STDOUT_LOG_MIN_PRIORITY: string; }; MEMPOOL: { HTTP_HOST: string; @@ -17,12 +18,20 @@ interface IConfig { MAX_PAGE_AGE?: number; RENDER_TIMEOUT?: number; }; + SYSLOG: { + ENABLED: boolean; + HOST: string; + PORT: number; + MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug'; + FACILITY: string; + }; } const defaults: IConfig = { 'SERVER': { 'HOST': 'http://localhost', 'HTTP_PORT': 4201, + 'STDOUT_LOG_MIN_PRIORITY': 'debug', }, 'MEMPOOL': { 'HTTP_HOST': 'http://localhost', @@ -32,18 +41,27 @@ const defaults: IConfig = { 'ENABLED': true, 'CLUSTER_SIZE': 1, }, + 'SYSLOG': { + 'ENABLED': true, + 'HOST': '127.0.0.1', + 'PORT': 514, + 'MIN_PRIORITY': 'info', + 'FACILITY': 'local7' + }, }; class Config implements IConfig { SERVER: IConfig['SERVER']; MEMPOOL: IConfig['MEMPOOL']; PUPPETEER: IConfig['PUPPETEER']; + SYSLOG: IConfig['SYSLOG']; constructor() { const configs = this.merge(configFile, defaults); this.SERVER = configs.SERVER; this.MEMPOOL = configs.MEMPOOL; this.PUPPETEER = configs.PUPPETEER; + this.SYSLOG = configs.SYSLOG; } merge = (...objects: object[]): IConfig => { diff --git a/unfurler/src/index.ts b/unfurler/src/index.ts index 4ae1d088e..60126863d 100644 --- a/unfurler/src/index.ts +++ b/unfurler/src/index.ts @@ -7,6 +7,7 @@ import { Cluster } from 'puppeteer-cluster'; import ReusablePage from './concurrency/ReusablePage'; import { parseLanguageUrl } from './language/lang'; import { matchRoute } from './routes'; +import logger from './logger'; const puppeteerConfig = require('../puppeteer.config.json'); if (config.PUPPETEER.EXEC_PATH) { @@ -55,7 +56,7 @@ class Server { this.server = http.createServer(this.app); this.server.listen(config.SERVER.HTTP_PORT, () => { - console.log(`Mempool Unfurl Server is running on port ${config.SERVER.HTTP_PORT}`); + logger.info(`Mempool Unfurl Server is running on port ${config.SERVER.HTTP_PORT}`); }); } @@ -102,20 +103,23 @@ class Server { // wait for preview component to initialize await page.waitForSelector('meta[property="og:preview:loading"]', { timeout: config.PUPPETEER.RENDER_TIMEOUT || 3000 }) - let success = false; + let success; success = await Promise.race([ page.waitForSelector('meta[property="og:preview:ready"]', { timeout: config.PUPPETEER.RENDER_TIMEOUT || 3000 }).then(() => true), page.waitForSelector('meta[property="og:preview:fail"]', { timeout: config.PUPPETEER.RENDER_TIMEOUT || 3000 }).then(() => false) ]) - if (success) { + if (success === true) { const screenshot = await page.screenshot(); return screenshot; + } else if (success === false) { + logger.warn(`failed to render page preview for ${action} due to client-side error, e.g. requested an invalid txid`); + page.repairRequested = true; } else { - console.log(`failed to render page preview for ${action} due to client-side error. probably requested an invalid ID`); + logger.warn(`failed to render page preview for ${action} due to puppeteer timeout`); page.repairRequested = true; } } catch (e) { - console.log(`failed to render page for ${action}`, e instanceof Error ? e.message : e); + logger.err(`failed to render page for ${action}: ` + (e instanceof Error ? e.message : `${e}`)); page.repairRequested = true; } } @@ -150,7 +154,7 @@ class Server { res.send(img); } } catch (e) { - console.log(e); + logger.err(e instanceof Error ? e.message : `${e}`); res.status(500).send(e instanceof Error ? e.message : e); } } @@ -202,7 +206,7 @@ class Server { const server = new Server(); process.on('SIGTERM', async () => { - console.info('Shutting down Mempool Unfurl Server'); + logger.info('Shutting down Mempool Unfurl Server'); await server.stopServer(); process.exit(0); }); diff --git a/unfurler/src/logger.ts b/unfurler/src/logger.ts new file mode 100644 index 000000000..e3b653b56 --- /dev/null +++ b/unfurler/src/logger.ts @@ -0,0 +1,142 @@ +import config from './config'; +import * as dgram from 'dgram'; + +class Logger { + static priorities = { + emerg: 0, + alert: 1, + crit: 2, + err: 3, + warn: 4, + notice: 5, + info: 6, + debug: 7 + }; + static facilities = { + kern: 0, + user: 1, + mail: 2, + daemon: 3, + auth: 4, + syslog: 5, + lpr: 6, + news: 7, + uucp: 8, + local0: 16, + local1: 17, + local2: 18, + local3: 19, + local4: 20, + local5: 21, + local6: 22, + local7: 23 + }; + + // @ts-ignore + public emerg: ((msg: string, quiet: boolean = false) => void); + // @ts-ignore + public alert: ((msg: string, quiet: boolean = false) => void); + // @ts-ignore + public crit: ((msg: string, quiet: boolean = false) => void); + // @ts-ignore + public err: ((msg: string, quiet: boolean = false) => void); + // @ts-ignore + public warn: ((msg: string, quiet: boolean = false) => void); + // @ts-ignore + public notice: ((msg: string, quiet: boolean = false) => void); + // @ts-ignore + public info: ((msg: string, quiet: boolean = false) => void); + // @ts-ignore + public debug: ((msg: string, quiet: boolean = false) => void); + + private name = 'mempool'; + private client: dgram.Socket; + private network: string; + + constructor() { + let prio; + for (prio in Logger.priorities) { + if (true) { + this.addprio(prio); + } + } + this.client = dgram.createSocket('udp4'); + this.network = this.getNetwork(); + } + + private addprio(prio): void { + this[prio] = (function(_this) { + return function(msg, quiet) { + return _this.msg(prio, msg, quiet); + }; + })(this); + } + + private getNetwork(): string { + return config.MEMPOOL.NETWORK || 'bitcoin'; + } + + private msg(priority, msg, quiet) { + let consolemsg, prionum, syslogmsg; + if (typeof msg === 'string' && msg.length > 0) { + while (msg[msg.length - 1].charCodeAt(0) === 10) { + msg = msg.slice(0, msg.length - 1); + } + } + const network = this.network ? ' <' + this.network + '-unfurler>' : ''; + prionum = Logger.priorities[priority] || Logger.priorities.info; + consolemsg = `${this.ts()} [${process.pid}] ${priority.toUpperCase()}:${network} ${msg}`; + + if (config.SYSLOG.ENABLED && Logger.priorities[priority] <= Logger.priorities[config.SYSLOG.MIN_PRIORITY]) { + syslogmsg = `<${(Logger.facilities[config.SYSLOG.FACILITY] * 8 + prionum)}> ${this.name}[${process.pid}]: ${priority.toUpperCase()}${network} ${msg}`; + this.syslog(syslogmsg); + } + if (quiet || Logger.priorities[priority] > Logger.priorities[config.SERVER.STDOUT_LOG_MIN_PRIORITY]) { + return; + } + if (priority === 'warning') { + priority = 'warn'; + } + if (priority === 'debug') { + priority = 'info'; + } + if (priority === 'err') { + priority = 'error'; + } + return (console[priority] || console.error)(consolemsg); + } + + private syslog(msg) { + let msgbuf; + msgbuf = Buffer.from(msg); + this.client.send(msgbuf, 0, msgbuf.length, config.SYSLOG.PORT, config.SYSLOG.HOST, function(err, bytes) { + if (err) { + console.log(err); + } + }); + } + + private leadZero(n: number): number | string { + if (n < 10) { + return '0' + n; + } + return n; + } + + private ts() { + let day, dt, hours, minutes, month, months, seconds; + dt = new Date(); + hours = this.leadZero(dt.getHours()); + minutes = this.leadZero(dt.getMinutes()); + seconds = this.leadZero(dt.getSeconds()); + month = dt.getMonth(); + day = dt.getDate(); + if (day < 10) { + day = ' ' + day; + } + months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + return months[month] + ' ' + day + ' ' + hours + ':' + minutes + ':' + seconds; + } +} + +export default new Logger(); diff --git a/unfurler/src/routes.ts b/unfurler/src/routes.ts index 4c25bf93b..3dfa66b5f 100644 --- a/unfurler/src/routes.ts +++ b/unfurler/src/routes.ts @@ -61,7 +61,16 @@ const routes = { }, mining: { title: "Mining", - fallbackImg: '/resources/previews/mining.png' + fallbackImg: '/resources/previews/mining.png', + routes: { + pool: { + render: true, + params: 1, + getTitle(path) { + return `Mining Pool: ${path[0]}`; + } + } + } } };