From a43f0454f91314edac826a88f9ea1f405e8ee45c Mon Sep 17 00:00:00 2001 From: softsimon Date: Sat, 17 Sep 2022 01:26:32 +0200 Subject: [PATCH] Mempool node group page --- backend/src/api/explorer/nodes.routes.ts | 34 +++++ .../app/lightning/group/group.component.html | 123 ++++++++++++++++++ .../app/lightning/group/group.component.scss | 52 ++++++++ .../app/lightning/group/group.component.ts | 103 +++++++++++++++ .../app/lightning/lightning-api.service.ts | 4 + .../lightning-dashboard.component.html | 7 + .../lightning-dashboard.component.ts | 3 + .../src/app/lightning/lightning.module.ts | 2 + .../app/lightning/lightning.routing.module.ts | 5 + .../nodes-map/nodes-map.component.ts | 4 +- 10 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/lightning/group/group.component.html create mode 100644 frontend/src/app/lightning/group/group.component.scss create mode 100644 frontend/src/app/lightning/group/group.component.ts 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/frontend/src/app/lightning/group/group.component.html b/frontend/src/app/lightning/group/group.component.html new file mode 100644 index 000000000..5264ac62a --- /dev/null +++ b/frontend/src/app/lightning/group/group.component.html @@ -0,0 +1,123 @@ +
+
+
+ +
+
+ +
+ +
+
+
+ + + + + + + + + + + + + + + +
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..4820a3123 --- /dev/null +++ b/frontend/src/app/lightning/group/group.component.scss @@ -0,0 +1,52 @@ +.logo-container { + width: 250px; + margin: auto; +} + +.header { + text-align: center; +} + +.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; +} + 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..e1f19f5ed --- /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 'src/app/services/seo.service'; +import { GeolocationData } from 'src/app/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..87cd7c5db 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..62e82a4d1 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts @@ -3,6 +3,7 @@ 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 { StateService } from 'src/app/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.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..f9f837a73 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/mempool.space', + component: GroupComponent, + }, { path: 'nodes/rankings', component: NodesRankingsDashboard, 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..531ac6c7b 100644 --- a/frontend/src/app/lightning/nodes-map/nodes-map.component.ts +++ b/frontend/src/app/lightning/nodes-map/nodes-map.component.ts @@ -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);