diff --git a/frontend/src/app/lightning/channel/channel.component.html b/frontend/src/app/lightning/channel/channel.component.html new file mode 100644 index 000000000..c50680147 --- /dev/null +++ b/frontend/src/app/lightning/channel/channel.component.html @@ -0,0 +1,144 @@ +
+
+

Channel {{ channel.id }}

+
+ Open +
+
+ +
+ +
+ +
+
+ + + + + + + + + + + +
Last update{{ channel.updated_at | date:'yyyy-MM-dd HH:mm' }}
Transaction ID + + {{ channel.transaction_id | shortenString : 10 }} + + +
+
+
+
+ + + + + + + +
Capacity
+
+
+ +
+ +
+

Peers

+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Node + {{ channel.alias_left }} +
+ + {{ channel.node1_public_key | shortenString : 18 }} + + +
Fee rate + {{ channel.node1_fee_rate / 10000 | number }}% +
Base fee + +
Min HTLC + +
Max HTLC + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Node + {{ channel.alias_right }} +
+ + {{ channel.node2_public_key | shortenString : 18 }} + + +
Fee rate + {{ channel.node2_fee_rate / 10000 | number }}% +
Base fee + +
Min HTLC + +
Max HTLC + +
+
+
+ +
+ + +
+ +
diff --git a/frontend/src/app/lightning/channel/channel.component.scss b/frontend/src/app/lightning/channel/channel.component.scss new file mode 100644 index 000000000..ccf88f131 --- /dev/null +++ b/frontend/src/app/lightning/channel/channel.component.scss @@ -0,0 +1,3 @@ +.badges { + font-size: 18px; +} \ No newline at end of file diff --git a/frontend/src/app/lightning/channel/channel.component.ts b/frontend/src/app/lightning/channel/channel.component.ts new file mode 100644 index 000000000..b64e08353 --- /dev/null +++ b/frontend/src/app/lightning/channel/channel.component.ts @@ -0,0 +1,30 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap } from '@angular/router'; +import { Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; +import { LightningApiService } from '../lightning-api.service'; + +@Component({ + selector: 'app-channel', + templateUrl: './channel.component.html', + styleUrls: ['./channel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ChannelComponent implements OnInit { + channel$: Observable; + + constructor( + private lightningApiService: LightningApiService, + private activatedRoute: ActivatedRoute, + ) { } + + ngOnInit(): void { + this.channel$ = this.activatedRoute.paramMap + .pipe( + switchMap((params: ParamMap) => { + return this.lightningApiService.getChannel$(params.get('short_id')); + }) + ); + } + +} diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.html b/frontend/src/app/lightning/channels-list/channels-list.component.html new file mode 100644 index 000000000..77073ca64 --- /dev/null +++ b/frontend/src/app/lightning/channels-list/channels-list.component.html @@ -0,0 +1,66 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Node AliasFee RateChannel IDCapacityTransaction ID
+ {{ channel.alias_left }} + + {{ channel.node1_fee_rate / 10000 | number }}% + + {{ channel.alias_right }} + + {{ channel.node2_fee_rate / 10000 | number }}% + + {{ channel.id }} + + + + + {{ channel.transaction_id | shortenString : 10 }} + + +
+ + + + + + + + + +
+ +
\ No newline at end of file diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.scss b/frontend/src/app/lightning/channels-list/channels-list.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.ts b/frontend/src/app/lightning/channels-list/channels-list.component.ts new file mode 100644 index 000000000..f6b0c448b --- /dev/null +++ b/frontend/src/app/lightning/channels-list/channels-list.component.ts @@ -0,0 +1,23 @@ +import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { LightningApiService } from '../lightning-api.service'; + +@Component({ + selector: 'app-channels-list', + templateUrl: './channels-list.component.html', + styleUrls: ['./channels-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ChannelsListComponent implements OnChanges { + @Input() publicKey: string; + channels$: Observable; + + constructor( + private lightningApiService: LightningApiService, + ) { } + + ngOnChanges(): void { + this.channels$ = this.lightningApiService.getChannelsByNodeId$(this.publicKey); + } + +} diff --git a/frontend/src/app/lightning/lightning-api.service.ts b/frontend/src/app/lightning/lightning-api.service.ts index aa8d1794f..326ac063f 100644 --- a/frontend/src/app/lightning/lightning-api.service.ts +++ b/frontend/src/app/lightning/lightning-api.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; const API_BASE_URL = '/lightning/api/v1'; @@ -16,8 +16,16 @@ export class LightningApiService { return this.httpClient.get(API_BASE_URL + '/nodes/' + publicKey); } + getChannel$(shortId: string): Observable { + return this.httpClient.get(API_BASE_URL + '/channels/' + shortId); + } + getChannelsByNodeId$(publicKey: string): Observable { - return this.httpClient.get(API_BASE_URL + '/channels/' + publicKey); + let params = new HttpParams() + .set('public_key', publicKey) + ; + + return this.httpClient.get(API_BASE_URL + '/channels', { params }); } getLatestStatistics$(): Observable { diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts index 4a4731f6d..3cdf2a281 100644 --- a/frontend/src/app/lightning/lightning.module.ts +++ b/frontend/src/app/lightning/lightning.module.ts @@ -8,12 +8,16 @@ import { RouterModule } from '@angular/router'; import { NodeStatisticsComponent } from './node-statistics/node-statistics.component'; import { NodeComponent } from './node/node.component'; import { LightningRoutingModule } from './lightning.routing.module'; +import { ChannelsListComponent } from './channels-list/channels-list.component'; +import { ChannelComponent } from './channel/channel.component'; @NgModule({ declarations: [ LightningDashboardComponent, NodesListComponent, NodeStatisticsComponent, NodeComponent, + ChannelsListComponent, + ChannelComponent, ], imports: [ CommonModule, diff --git a/frontend/src/app/lightning/lightning.routing.module.ts b/frontend/src/app/lightning/lightning.routing.module.ts index 456436c8d..c04e34f23 100644 --- a/frontend/src/app/lightning/lightning.routing.module.ts +++ b/frontend/src/app/lightning/lightning.routing.module.ts @@ -2,6 +2,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { LightningDashboardComponent } from './lightning-dashboard/lightning-dashboard.component'; import { NodeComponent } from './node/node.component'; +import { ChannelComponent } from './channel/channel.component'; const routes: Routes = [ { @@ -12,6 +13,10 @@ const routes: Routes = [ path: 'node/:public_key', component: NodeComponent, }, + { + path: 'channel/:short_id', + component: ChannelComponent, + }, { path: '**', redirectTo: '' diff --git a/frontend/src/app/lightning/node-statistics/node-statistics.component.html b/frontend/src/app/lightning/node-statistics/node-statistics.component.html index bac937093..0bfbce647 100644 --- a/frontend/src/app/lightning/node-statistics/node-statistics.component.html +++ b/frontend/src/app/lightning/node-statistics/node-statistics.component.html @@ -4,7 +4,7 @@
Capacity
- + diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index 458c9362e..c85f63c78 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -1 +1,66 @@ -

node works!

+
+ + +
+ +
+ +
+
+ + + + + + + + + + + + + + + +
First Seen{{ node.first_seen | date:'yyyy-MM-dd HH:mm' }}
Updated At{{ node.updated_at | date:'yyyy-MM-dd HH:mm' }}
Color
{{ node.color }}
+
+
+
+
+ +
+
+
+ +
+ +
+

Channels

+ + + + + +
+ +
diff --git a/frontend/src/app/lightning/node/node.component.scss b/frontend/src/app/lightning/node/node.component.scss index e69de29bb..5ff5de482 100644 --- a/frontend/src/app/lightning/node/node.component.scss +++ b/frontend/src/app/lightning/node/node.component.scss @@ -0,0 +1,38 @@ +.qr-wrapper { + background-color: #FFF; + padding: 10px; + padding-bottom: 5px; + display: inline-block; +} + +.qrcode-col { + margin: 20px auto 10px; + text-align: center; + @media (min-width: 992px){ + margin: 0px auto 0px; + } +} + +.tx-link { + display: flex; + flex-grow: 1; + @media (min-width: 650px) { + align-self: end; + margin-left: 15px; + margin-top: 0px; + margin-bottom: -3px; + } + @media (min-width: 768px) { + margin-bottom: 4px; + top: 1px; + position: relative; + } + @media (max-width: 768px) { + order: 3; + } +} + +.title-container { + display: flex; + flex-direction: row; +} diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index bfbc3d134..b62198d0d 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; @@ -7,10 +7,12 @@ import { LightningApiService } from '../lightning-api.service'; @Component({ selector: 'app-node', templateUrl: './node.component.html', - styleUrls: ['./node.component.scss'] + styleUrls: ['./node.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class NodeComponent implements OnInit { node$: Observable; + publicKey$: Observable; constructor( private lightningApiService: LightningApiService, @@ -21,7 +23,7 @@ export class NodeComponent implements OnInit { this.node$ = this.activatedRoute.paramMap .pipe( switchMap((params: ParamMap) => { - return this.lightningApiService.getNode$(params.get('id')); + return this.lightningApiService.getNode$(params.get('public_key')); }) ); } diff --git a/frontend/src/app/shared/components/sats/sats.component.html b/frontend/src/app/shared/components/sats/sats.component.html new file mode 100644 index 000000000..8358812a9 --- /dev/null +++ b/frontend/src/app/shared/components/sats/sats.component.html @@ -0,0 +1,5 @@ +‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number }} +L- +tL- +t- +s-sats diff --git a/frontend/src/app/shared/components/sats/sats.component.scss b/frontend/src/app/shared/components/sats/sats.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/shared/components/sats/sats.component.ts b/frontend/src/app/shared/components/sats/sats.component.ts new file mode 100644 index 000000000..f341b3027 --- /dev/null +++ b/frontend/src/app/shared/components/sats/sats.component.ts @@ -0,0 +1,32 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { StateService } from '../../../services/state.service'; + +@Component({ + selector: 'app-sats', + templateUrl: './sats.component.html', + styleUrls: ['./sats.component.scss'] +}) +export class SatsComponent implements OnInit { + @Input() satoshis: number; + @Input() digitsInfo = 0; + @Input() addPlus = false; + + network = ''; + stateSubscription: Subscription; + + constructor( + private stateService: StateService, + ) { } + + ngOnInit() { + this.stateSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network); + } + + ngOnDestroy() { + if (this.stateSubscription) { + this.stateSubscription.unsubscribe(); + } + } + +} diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index a587c6934..7e8914000 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -74,6 +74,7 @@ import { LoadingIndicatorComponent } from '../components/loading-indicator/loadi import { IndexingProgressComponent } from '../components/indexing-progress/indexing-progress.component'; import { SvgImagesComponent } from '../components/svg-images/svg-images.component'; import { ChangeComponent } from '../components/change/change.component'; +import { SatsComponent } from './components/sats/sats.component'; @NgModule({ declarations: [ @@ -142,6 +143,7 @@ import { ChangeComponent } from '../components/change/change.component'; IndexingProgressComponent, SvgImagesComponent, ChangeComponent, + SatsComponent, ], imports: [ CommonModule, @@ -238,6 +240,7 @@ import { ChangeComponent } from '../components/change/change.component'; IndexingProgressComponent, SvgImagesComponent, ChangeComponent, + SatsComponent ] }) export class SharedModule { diff --git a/lightning-backend/src/api/nodes/channels.api.ts b/lightning-backend/src/api/nodes/channels.api.ts index 6b4905bd7..9f02981c3 100644 --- a/lightning-backend/src/api/nodes/channels.api.ts +++ b/lightning-backend/src/api/nodes/channels.api.ts @@ -2,9 +2,20 @@ import logger from '../../logger'; import DB from '../../database'; class ChannelsApi { + public async $getChannel(shortId: string): Promise { + try { + const query = `SELECT n1.alias AS alias_left, n2.alias AS alias_right, channels.* FROM channels LEFT JOIN nodes AS n1 ON n1.public_key = channels.node1_public_key LEFT JOIN nodes AS n2 ON n2.public_key = channels.node2_public_key WHERE channels.id = ?`; + const [rows]: any = await DB.query(query, [shortId]); + return rows[0]; + } catch (e) { + logger.err('$getChannel error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } + public async $getChannelsForNode(public_key: string): Promise { try { - const query = `SELECT * FROM channels WHERE node1_public_key = ? OR node2_public_key = ?`; + const query = `SELECT n1.alias AS alias_left, n2.alias AS alias_right, channels.* FROM channels LEFT JOIN nodes AS n1 ON n1.public_key = channels.node1_public_key LEFT JOIN nodes AS n2 ON n2.public_key = channels.node2_public_key WHERE node1_public_key = ? OR node2_public_key = ?`; const [rows]: any = await DB.query(query, [public_key, public_key]); return rows; } catch (e) { diff --git a/lightning-backend/src/api/nodes/channels.routes.ts b/lightning-backend/src/api/nodes/channels.routes.ts index 70bf0bdad..4cb3f8b1d 100644 --- a/lightning-backend/src/api/nodes/channels.routes.ts +++ b/lightning-backend/src/api/nodes/channels.routes.ts @@ -3,16 +3,36 @@ import { Express, Request, Response } from 'express'; import channelsApi from './channels.api'; class ChannelsRoutes { - constructor(app: Express) { + constructor() { } + + public initRoutes(app: Express) { app - .get(config.MEMPOOL.API_URL_PREFIX + 'channels/:public_key', this.$getChannels) + .get(config.MEMPOOL.API_URL_PREFIX + 'channels/:short_id', this.$getChannel) + .get(config.MEMPOOL.API_URL_PREFIX + 'channels', this.$getChannels) ; } + private async $getChannel(req: Request, res: Response) { + try { + const channel = await channelsApi.$getChannel(req.params.short_id); + if (!channel) { + res.status(404).send('Channel not found'); + return; + } + res.json(channel); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + private async $getChannels(req: Request, res: Response) { try { - const channels = await channelsApi.$getChannelsForNode(req.params.public_key); - res.json(channels); + if (typeof req.query.public_key !== 'string') { + res.status(501).send('Missing parameter: public_key'); + return; + } + const channels = await channelsApi.$getChannelsForNode(req.query.public_key); + res.json(channels); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); } @@ -20,4 +40,4 @@ class ChannelsRoutes { } -export default ChannelsRoutes; +export default new ChannelsRoutes(); diff --git a/lightning-backend/src/api/nodes/nodes.routes.ts b/lightning-backend/src/api/nodes/nodes.routes.ts index ad254959c..73bef9f26 100644 --- a/lightning-backend/src/api/nodes/nodes.routes.ts +++ b/lightning-backend/src/api/nodes/nodes.routes.ts @@ -1,14 +1,15 @@ import config from '../../config'; import { Express, Request, Response } from 'express'; import nodesApi from './nodes.api'; -import channelsApi from './channels.api'; class NodesRoutes { - constructor(app: Express) { + constructor() { } + + public initRoutes(app: Express) { app - .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/latest', this.$getGeneralStats) - .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/top', this.$getTopNodes) - .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/:public_key', this.$getNode) - ; + .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/latest', this.$getGeneralStats) + .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/top', this.$getTopNodes) + .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/:public_key', this.$getNode) + ; } private async $getNode(req: Request, res: Response) { @@ -47,4 +48,4 @@ class NodesRoutes { } } -export default NodesRoutes; +export default new NodesRoutes(); diff --git a/lightning-backend/src/index.ts b/lightning-backend/src/index.ts index 49275f782..614ac8499 100644 --- a/lightning-backend/src/index.ts +++ b/lightning-backend/src/index.ts @@ -1,21 +1,14 @@ import config from './config'; -import * as express from 'express'; -import * as http from 'http'; import logger from './logger'; import DB from './database'; -import { Express, Request, Response, NextFunction } from 'express'; import databaseMigration from './database-migration'; import statsUpdater from './tasks/stats-updater.service'; import nodeSyncService from './tasks/node-sync.service'; -import NodesRoutes from './api/nodes/nodes.routes'; -import ChannelsRoutes from './api/nodes/channels.routes'; +import server from './server'; logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`); class LightningServer { - private server: http.Server | undefined; - private app: Express = express(); - constructor() { this.init(); } @@ -27,27 +20,7 @@ class LightningServer { statsUpdater.startService(); nodeSyncService.startService(); - this.startServer(); - } - - startServer() { - this.app - .use((req: Request, res: Response, next: NextFunction) => { - res.setHeader('Access-Control-Allow-Origin', '*'); - next(); - }) - .use(express.urlencoded({ extended: true })) - .use(express.text()) - ; - - this.server = http.createServer(this.app); - - this.server.listen(config.MEMPOOL.HTTP_PORT, () => { - logger.notice(`Mempool Lightning is running on port ${config.MEMPOOL.HTTP_PORT}`); - }); - - const nodeRoutes = new NodesRoutes(this.app); - const channelsRoutes = new ChannelsRoutes(this.app); + server.startServer(); } } diff --git a/lightning-backend/src/server.ts b/lightning-backend/src/server.ts new file mode 100644 index 000000000..26608b0c4 --- /dev/null +++ b/lightning-backend/src/server.ts @@ -0,0 +1,38 @@ +import { Express, Request, Response, NextFunction } from 'express'; +import * as express from 'express'; +import * as http from 'http'; +import logger from './logger'; +import config from './config'; +import nodesRoutes from './api/nodes/nodes.routes'; +import channelsRoutes from './api/nodes/channels.routes'; + +class Server { + private server: http.Server | undefined; + private app: Express = express(); + + public startServer() { + this.app + .use((req: Request, res: Response, next: NextFunction) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + next(); + }) + .use(express.urlencoded({ extended: true })) + .use(express.text()) + ; + + this.server = http.createServer(this.app); + + this.server.listen(config.MEMPOOL.HTTP_PORT, () => { + logger.notice(`Mempool Lightning is running on port ${config.MEMPOOL.HTTP_PORT}`); + }); + + this.initRoutes(); + } + + private initRoutes() { + nodesRoutes.initRoutes(this.app); + channelsRoutes.initRoutes(this.app); + } +} + +export default new Server(); diff --git a/lightning-backend/src/tasks/stats-updater.service.ts b/lightning-backend/src/tasks/stats-updater.service.ts index c96a3d6b4..0c61922e9 100644 --- a/lightning-backend/src/tasks/stats-updater.service.ts +++ b/lightning-backend/src/tasks/stats-updater.service.ts @@ -15,12 +15,13 @@ class LightningStatsUpdater { setTimeout(() => { this.$logLightningStats(); - this.$logNodeStatsDaily(); setInterval(() => { this.$logLightningStats(); this.$logNodeStatsDaily(); }, 1000 * 60 * 60); }, difference); + + this.$logNodeStatsDaily(); } private async $logNodeStatsDaily() { @@ -28,7 +29,7 @@ class LightningStatsUpdater { try { const [state]: any = await DB.query(`SELECT string FROM state WHERE name = 'last_node_stats'`); // Only store once per day - if (state[0] === currentDate) { + if (state[0].string === currentDate) { return; } @@ -42,6 +43,7 @@ class LightningStatsUpdater { node.channels_count_left, node.channels_count_right]); } await DB.query(`UPDATE state SET string = ? WHERE name = 'last_node_stats'`, [currentDate]); + logger.debug('Daily node stats has updated.'); } catch (e) { logger.err('$logNodeStatsDaily() error: ' + (e instanceof Error ? e.message : e)); } @@ -49,22 +51,26 @@ class LightningStatsUpdater { private async $logLightningStats() { try { - const networkInfo = await lightningApi.$getNetworkInfo(); + const networkGraph = await lightningApi.$getNetworkGraph(); + let total_capacity = 0; + for (const channel of networkGraph.channels) { + if (channel.capacity) { + total_capacity += channel.capacity; + } + } const query = `INSERT INTO statistics( added, channel_count, node_count, - total_capacity, - average_channel_size + total_capacity ) - VALUES (NOW(), ?, ?, ?, ?)`; + VALUES (NOW(), ?, ?, ?)`; await DB.query(query, [ - networkInfo.channel_count, - networkInfo.node_count, - networkInfo.total_capacity, - networkInfo.average_channel_size + networkGraph.channels.length, + networkGraph.nodes.length, + total_capacity, ]); } catch (e) { logger.err('$logLightningStats() error: ' + (e instanceof Error ? e.message : e));