Node and channel API
This commit is contained in:
parent
8d622e3606
commit
f5325b3a6d
@ -12,6 +12,14 @@ export class LightningApiService {
|
|||||||
private httpClient: HttpClient,
|
private httpClient: HttpClient,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
getNode$(publicKey: string): Observable<any> {
|
||||||
|
return this.httpClient.get<any>(API_BASE_URL + '/nodes/' + publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
getChannelsByNodeId$(publicKey: string): Observable<any> {
|
||||||
|
return this.httpClient.get<any>(API_BASE_URL + '/channels/' + publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
getLatestStatistics$(): Observable<any> {
|
getLatestStatistics$(): Observable<any> {
|
||||||
return this.httpClient.get<any>(API_BASE_URL + '/statistics/latest');
|
return this.httpClient.get<any>(API_BASE_URL + '/statistics/latest');
|
||||||
}
|
}
|
||||||
|
@ -6,16 +6,20 @@ import { LightningApiService } from './lightning-api.service';
|
|||||||
import { NodesListComponent } from './nodes-list/nodes-list.component';
|
import { NodesListComponent } from './nodes-list/nodes-list.component';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { NodeStatisticsComponent } from './node-statistics/node-statistics.component';
|
import { NodeStatisticsComponent } from './node-statistics/node-statistics.component';
|
||||||
|
import { NodeComponent } from './node/node.component';
|
||||||
|
import { LightningRoutingModule } from './lightning.routing.module';
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
LightningDashboardComponent,
|
LightningDashboardComponent,
|
||||||
NodesListComponent,
|
NodesListComponent,
|
||||||
NodeStatisticsComponent,
|
NodeStatisticsComponent,
|
||||||
|
NodeComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
|
LightningRoutingModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
LightningApiService,
|
LightningApiService,
|
||||||
|
25
frontend/src/app/lightning/lightning.routing.module.ts
Normal file
25
frontend/src/app/lightning/lightning.routing.module.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: LightningDashboardComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'node/:public_key',
|
||||||
|
component: NodeComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '**',
|
||||||
|
redirectTo: ''
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class LightningRoutingModule { }
|
@ -1,5 +1,15 @@
|
|||||||
<div class="fee-estimation-wrapper" *ngIf="statistics$ | async as statistics; else loadingReward">
|
<div class="fee-estimation-wrapper" *ngIf="statistics$ | async as statistics; else loadingReward">
|
||||||
<div class="fee-estimation-container">
|
<div class="fee-estimation-container">
|
||||||
|
<div class="item">
|
||||||
|
<h5 class="card-title" i18n="mining.average-fee">Capacity</h5>
|
||||||
|
<div class="card-text" i18n-ngbTooltip="mining.average-fee"
|
||||||
|
ngbTooltip="Fee paid on average for each transaction in the past 144 blocks" placement="bottom">
|
||||||
|
<app-amount [satoshis]="statistics.latest.total_capacity" digitsInfo="1.2-3"></app-amount>
|
||||||
|
<span class="fiat">
|
||||||
|
<app-change [current]="statistics.latest.total_capacity" [previous]="statistics.previous.total_capacity"></app-change>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<h5 class="card-title" i18n="mining.rewards">Nodes</h5>
|
<h5 class="card-title" i18n="mining.rewards">Nodes</h5>
|
||||||
<div class="card-text" i18n-ngbTooltip="mining.rewards-desc"
|
<div class="card-text" i18n-ngbTooltip="mining.rewards-desc"
|
||||||
@ -24,6 +34,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!--
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<h5 class="card-title" i18n="mining.average-fee">Average Channel</h5>
|
<h5 class="card-title" i18n="mining.average-fee">Average Channel</h5>
|
||||||
<div class="card-text" i18n-ngbTooltip="mining.average-fee"
|
<div class="card-text" i18n-ngbTooltip="mining.average-fee"
|
||||||
@ -34,6 +45,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
1
frontend/src/app/lightning/node/node.component.html
Normal file
1
frontend/src/app/lightning/node/node.component.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<p>node works!</p>
|
0
frontend/src/app/lightning/node/node.component.scss
Normal file
0
frontend/src/app/lightning/node/node.component.scss
Normal file
29
frontend/src/app/lightning/node/node.component.ts
Normal file
29
frontend/src/app/lightning/node/node.component.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { 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-node',
|
||||||
|
templateUrl: './node.component.html',
|
||||||
|
styleUrls: ['./node.component.scss']
|
||||||
|
})
|
||||||
|
export class NodeComponent implements OnInit {
|
||||||
|
node$: Observable<any>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private lightningApiService: LightningApiService,
|
||||||
|
private activatedRoute: ActivatedRoute,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.node$ = this.activatedRoute.paramMap
|
||||||
|
.pipe(
|
||||||
|
switchMap((params: ParamMap) => {
|
||||||
|
return this.lightningApiService.getNode$(params.get('id'));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -9,7 +9,7 @@
|
|||||||
<tbody *ngIf="nodes$ | async as nodes; else skeleton">
|
<tbody *ngIf="nodes$ | async as nodes; else skeleton">
|
||||||
<tr *ngFor="let node of nodes; let i = index;">
|
<tr *ngFor="let node of nodes; let i = index;">
|
||||||
<td class="alias text-left">
|
<td class="alias text-left">
|
||||||
<a [routerLink]="['/lightning/nodes/:public_key' | relativeUrl, node.public]">{{ node.alias }}</a>
|
<a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">{{ node.alias }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="capacity text-right">
|
<td class="capacity text-right">
|
||||||
<app-amount [satoshis]="node.capacity_left + node.capacity_right" digitsInfo="1.2-2"></app-amount>
|
<app-amount [satoshis]="node.capacity_left + node.capacity_right" digitsInfo="1.2-2"></app-amount>
|
||||||
|
17
lightning-backend/src/api/nodes/channels.api.ts
Normal file
17
lightning-backend/src/api/nodes/channels.api.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import logger from '../../logger';
|
||||||
|
import DB from '../../database';
|
||||||
|
|
||||||
|
class ChannelsApi {
|
||||||
|
public async $getChannelsForNode(public_key: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const query = `SELECT * FROM channels WHERE node1_public_key = ? OR node2_public_key = ?`;
|
||||||
|
const [rows]: any = await DB.query(query, [public_key, public_key]);
|
||||||
|
return rows;
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$getChannelsForNode error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ChannelsApi();
|
23
lightning-backend/src/api/nodes/channels.routes.ts
Normal file
23
lightning-backend/src/api/nodes/channels.routes.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import config from '../../config';
|
||||||
|
import { Express, Request, Response } from 'express';
|
||||||
|
import channelsApi from './channels.api';
|
||||||
|
|
||||||
|
class ChannelsRoutes {
|
||||||
|
constructor(app: Express) {
|
||||||
|
app
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'channels/:public_key', this.$getChannels)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getChannels(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const channels = await channelsApi.$getChannelsForNode(req.params.public_key);
|
||||||
|
res.json(channels);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChannelsRoutes;
|
@ -2,9 +2,20 @@ import logger from '../../logger';
|
|||||||
import DB from '../../database';
|
import DB from '../../database';
|
||||||
|
|
||||||
class NodesApi {
|
class NodesApi {
|
||||||
|
public async $getNode(public_key: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const query = `SELECT * FROM nodes WHERE public_key = ?`;
|
||||||
|
const [rows]: any = await DB.query(query, [public_key]);
|
||||||
|
return rows[0];
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$getNode error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async $getTopCapacityNodes(): Promise<any> {
|
public async $getTopCapacityNodes(): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const query = `SELECT nodes.*, nodes_stats.capacity_left, nodes_stats.capacity_right, nodes_stats.channels_left, nodes_stats.channels_right FROM nodes LEFT JOIN nodes_stats ON nodes_stats.public_key = nodes.public_key ORDER BY nodes_stats.capacity_left + nodes_stats.capacity_right DESC LIMIT 10`;
|
const query = `SELECT nodes.*, nodes_stats.capacity_left, nodes_stats.capacity_right, nodes_stats.channels_left, nodes_stats.channels_right FROM nodes LEFT JOIN nodes_stats ON nodes_stats.public_key = nodes.public_key ORDER BY nodes_stats.added DESC, nodes_stats.capacity_left + nodes_stats.capacity_right DESC LIMIT 10`;
|
||||||
const [rows]: any = await DB.query(query);
|
const [rows]: any = await DB.query(query);
|
||||||
return rows;
|
return rows;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -15,7 +26,7 @@ class NodesApi {
|
|||||||
|
|
||||||
public async $getTopChannelsNodes(): Promise<any> {
|
public async $getTopChannelsNodes(): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const query = `SELECT nodes.*, nodes_stats.capacity_left, nodes_stats.capacity_right, nodes_stats.channels_left, nodes_stats.channels_right FROM nodes LEFT JOIN nodes_stats ON nodes_stats.public_key = nodes.public_key ORDER BY nodes_stats.channels_left + nodes_stats.channels_right DESC LIMIT 10`;
|
const query = `SELECT nodes.*, nodes_stats.capacity_left, nodes_stats.capacity_right, nodes_stats.channels_left, nodes_stats.channels_right FROM nodes LEFT JOIN nodes_stats ON nodes_stats.public_key = nodes.public_key ORDER BY nodes_stats.added DESC, nodes_stats.channels_left + nodes_stats.channels_right DESC LIMIT 10`;
|
||||||
const [rows]: any = await DB.query(query);
|
const [rows]: any = await DB.query(query);
|
||||||
return rows;
|
return rows;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -27,13 +38,13 @@ class NodesApi {
|
|||||||
public async $getLatestStatistics(): Promise<any> {
|
public async $getLatestStatistics(): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const [rows]: any = await DB.query(`SELECT * FROM statistics ORDER BY id DESC LIMIT 1`);
|
const [rows]: any = await DB.query(`SELECT * FROM statistics ORDER BY id DESC LIMIT 1`);
|
||||||
const [rows2]: any = await DB.query(`SELECT * FROM statistics ORDER BY id DESC LIMIT 1 OFFSET 24`);
|
const [rows2]: any = await DB.query(`SELECT * FROM statistics ORDER BY id DESC LIMIT 1 OFFSET 71`);
|
||||||
return {
|
return {
|
||||||
latest: rows[0],
|
latest: rows[0],
|
||||||
previous: rows2[0],
|
previous: rows2[0],
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$getTopChannelsNodes error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$getLatestStatistics error: ' + (e instanceof Error ? e.message : e));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,29 @@
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import { Express, Request, Response } from 'express';
|
import { Express, Request, Response } from 'express';
|
||||||
import nodesApi from './nodes.api';
|
import nodesApi from './nodes.api';
|
||||||
|
import channelsApi from './channels.api';
|
||||||
class NodesRoutes {
|
class NodesRoutes {
|
||||||
constructor(app: Express) {
|
constructor(app: Express) {
|
||||||
app
|
app
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/latest', this.$getGeneralStats)
|
.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/top', this.$getTopNodes)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'nodes/:public_key', this.$getNode)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async $getNode(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const node = await nodesApi.$getNode(req.params.public_key);
|
||||||
|
if (!node) {
|
||||||
|
res.status(404).send('Node not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json(node);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async $getGeneralStats(req: Request, res: Response) {
|
private async $getGeneralStats(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const statistics = await nodesApi.$getLatestStatistics();
|
const statistics = await nodesApi.$getLatestStatistics();
|
||||||
|
@ -125,6 +125,7 @@ class DatabaseMigration {
|
|||||||
|
|
||||||
// Set initial values
|
// Set initial values
|
||||||
await this.$executeQuery(`INSERT INTO state VALUES('schema_version', 0, NULL);`);
|
await this.$executeQuery(`INSERT INTO state VALUES('schema_version', 0, NULL);`);
|
||||||
|
await this.$executeQuery(`INSERT INTO state VALUES('last_node_stats', 0, '1970-01-01');`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import databaseMigration from './database-migration';
|
|||||||
import statsUpdater from './tasks/stats-updater.service';
|
import statsUpdater from './tasks/stats-updater.service';
|
||||||
import nodeSyncService from './tasks/node-sync.service';
|
import nodeSyncService from './tasks/node-sync.service';
|
||||||
import NodesRoutes from './api/nodes/nodes.routes';
|
import NodesRoutes from './api/nodes/nodes.routes';
|
||||||
|
import ChannelsRoutes from './api/nodes/channels.routes';
|
||||||
|
|
||||||
logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`);
|
logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`);
|
||||||
|
|
||||||
@ -46,6 +47,7 @@ class LightningServer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const nodeRoutes = new NodesRoutes(this.app);
|
const nodeRoutes = new NodesRoutes(this.app);
|
||||||
|
const channelsRoutes = new ChannelsRoutes(this.app);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,22 +15,35 @@ class LightningStatsUpdater {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$logLightningStats();
|
this.$logLightningStats();
|
||||||
|
this.$logNodeStatsDaily();
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
this.$logLightningStats();
|
this.$logLightningStats();
|
||||||
|
this.$logNodeStatsDaily();
|
||||||
}, 1000 * 60 * 60);
|
}, 1000 * 60 * 60);
|
||||||
}, difference);
|
}, difference);
|
||||||
|
|
||||||
// this.$logNodeStatsDaily();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $logNodeStatsDaily() {
|
private async $logNodeStatsDaily() {
|
||||||
const query = `SELECT nodes.public_key, COUNT(DISTINCT c1.id) AS channels_count_left, COUNT(DISTINCT c2.id) AS channels_count_right, SUM(DISTINCT c1.capacity) AS channels_capacity_left, SUM(DISTINCT c2.capacity) AS channels_capacity_right FROM nodes LEFT JOIN channels AS c1 ON c1.node1_public_key = nodes.public_key LEFT JOIN channels AS c2 ON c2.node2_public_key = nodes.public_key GROUP BY nodes.public_key`;
|
const currentDate = new Date().toISOString().split('T')[0];
|
||||||
const [nodes]: any = await DB.query(query);
|
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) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const node of nodes) {
|
const query = `SELECT nodes.public_key, c1.channels_count_left, c2.channels_count_right, c1.channels_capacity_left, c2.channels_capacity_right FROM nodes LEFT JOIN (SELECT node1_public_key, COUNT(id) AS channels_count_left, SUM(capacity) AS channels_capacity_left FROM channels GROUP BY node1_public_key) c1 ON c1.node1_public_key = nodes.public_key LEFT JOIN (SELECT node2_public_key, COUNT(id) AS channels_count_right, SUM(capacity) AS channels_capacity_right FROM channels GROUP BY node2_public_key) c2 ON c2.node2_public_key = nodes.public_key`;
|
||||||
await DB.query(
|
const [nodes]: any = await DB.query(query);
|
||||||
`INSERT INTO nodes_stats(public_key, added, capacity_left, capacity_right, channels_left, channels_right) VALUES (?, NOW(), ?, ?, ?, ?)`,
|
|
||||||
[node.public_key, node.channels_capacity_left, node.channels_capacity_right, node.channels_count_left, node.channels_count_right]);
|
for (const node of nodes) {
|
||||||
|
await DB.query(
|
||||||
|
`INSERT INTO nodes_stats(public_key, added, capacity_left, capacity_right, channels_left, channels_right) VALUES (?, NOW(), ?, ?, ?, ?)`,
|
||||||
|
[node.public_key, node.channels_capacity_left, node.channels_capacity_right,
|
||||||
|
node.channels_count_left, node.channels_count_right]);
|
||||||
|
}
|
||||||
|
await DB.query(`UPDATE state SET string = ? WHERE name = 'last_node_stats'`, [currentDate]);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$logNodeStatsDaily() error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user