Store and display stats and node top lists
This commit is contained in:
		
							parent
							
								
									3e6af8e87b
								
							
						
					
					
						commit
						8d622e3606
					
				@ -16,5 +16,6 @@
 | 
			
		||||
  "MEMPOOL_WEBSITE_URL": "https://mempool.space",
 | 
			
		||||
  "LIQUID_WEBSITE_URL": "https://liquid.network",
 | 
			
		||||
  "BISQ_WEBSITE_URL": "https://bisq.markets",
 | 
			
		||||
  "MINING_DASHBOARD": true
 | 
			
		||||
  "MINING_DASHBOARD": true,
 | 
			
		||||
  "LIGHTNING": false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -102,6 +102,16 @@ if (configContent && configContent.BASE_MODULE === 'bisq') {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PROXY_CONFIG.push(...[
 | 
			
		||||
  {
 | 
			
		||||
    context: ['/lightning/api/v1/**'],
 | 
			
		||||
    target: `http://localhost:8899`,
 | 
			
		||||
    secure: false,
 | 
			
		||||
    changeOrigin: true,
 | 
			
		||||
    proxyTimeout: 30000,
 | 
			
		||||
    pathRewrite: {
 | 
			
		||||
        "^/lightning/api": "/api"
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    context: ['/api/v1/**'],
 | 
			
		||||
    target: `http://localhost:8999`,
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@ import { AssetsFeaturedComponent } from './components/assets/assets-featured/ass
 | 
			
		||||
import { AssetsComponent } from './components/assets/assets.component';
 | 
			
		||||
import { AssetComponent } from './components/asset/asset.component';
 | 
			
		||||
import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component';
 | 
			
		||||
import { LightningDashboardComponent } from './lightning/lightning-dashboard/lightning-dashboard.component';
 | 
			
		||||
 | 
			
		||||
let routes: Routes = [
 | 
			
		||||
  { 
 | 
			
		||||
@ -96,6 +97,11 @@ let routes: Routes = [
 | 
			
		||||
            path: 'api',
 | 
			
		||||
            loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'lightning',
 | 
			
		||||
            component: LightningDashboardComponent,
 | 
			
		||||
            loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
@ -186,6 +192,11 @@ let routes: Routes = [
 | 
			
		||||
            path: 'api',
 | 
			
		||||
            loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'lightning',
 | 
			
		||||
            component: LightningDashboardComponent,
 | 
			
		||||
            loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
@ -273,6 +284,11 @@ let routes: Routes = [
 | 
			
		||||
        path: 'api',
 | 
			
		||||
        loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'lightning',
 | 
			
		||||
        component: LightningDashboardComponent,
 | 
			
		||||
        loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								frontend/src/app/components/change/change.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/app/components/change/change.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
<span [style]="change >= 0 ? 'color: #42B747' : 'color: #B74242'">
 | 
			
		||||
  {{ change >= 0 ? '+' : '' }}{{ change | amountShortener }}%
 | 
			
		||||
</span>
 | 
			
		||||
							
								
								
									
										21
									
								
								frontend/src/app/components/change/change.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								frontend/src/app/components/change/change.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-change',
 | 
			
		||||
  templateUrl: './change.component.html',
 | 
			
		||||
  styleUrls: ['./change.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class ChangeComponent implements OnChanges {
 | 
			
		||||
  @Input() current: number;
 | 
			
		||||
  @Input() previous: number;
 | 
			
		||||
 | 
			
		||||
  change: number;
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(): void {
 | 
			
		||||
    this.change = (this.current - this.previous) / this.previous * 100;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -35,15 +35,15 @@
 | 
			
		||||
      <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="mining.mining-dashboard" title="Mining Dashboard"></fa-icon></a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-pools" *ngIf="stateService.env.LIGHTNING">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/lightning' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'bolt']" [fixedWidth]="true" i18n-title="master-page.lightning" title="Lightning Explorer"></fa-icon></a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li class="nav-item" routerLinkActive="active" id="btn-blocks" *ngIf="!stateService.env.MINING_DASHBOARD">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/blocks' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'cubes']" [fixedWidth]="true" i18n-title="master-page.blocks" title="Blocks"></fa-icon></a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li class="nav-item" routerLinkActive="active" id="btn-graphs">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/graphs' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'chart-area']" [fixedWidth]="true" i18n-title="master-page.graphs" title="Graphs"></fa-icon></a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li class="nav-item d-none d-lg-block" routerLinkActive="active" id="btn-tv">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/tv' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon></a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li class="nav-item" routerLinkActive="active" id="btn-docs">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/docs' | relativeUrl ]" (click)="collapse()"><fa-icon [icon]="['fas', 'book']" [fixedWidth]="true" i18n-title="documentation.title" title="Documentation"></fa-icon></a>
 | 
			
		||||
      </li>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										22
									
								
								frontend/src/app/lightning/lightning-api.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								frontend/src/app/lightning/lightning-api.service.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { HttpClient } from '@angular/common/http';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
const API_BASE_URL = '/lightning/api/v1';
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
  providedIn: 'root'
 | 
			
		||||
})
 | 
			
		||||
export class LightningApiService {
 | 
			
		||||
  constructor(
 | 
			
		||||
    private httpClient: HttpClient,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  getLatestStatistics$(): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any>(API_BASE_URL + '/statistics/latest');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  listTopNodes$(): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any>(API_BASE_URL + '/nodes/top');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,48 @@
 | 
			
		||||
<div class="container-xl dashboard-container">
 | 
			
		||||
 | 
			
		||||
  <div class="row row-cols-1 row-cols-md-2">
 | 
			
		||||
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="main-title">
 | 
			
		||||
        <span i18n="lightning.statistics-title">Nodes Statistics</span> 
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="card-wrapper">
 | 
			
		||||
        <div class="card" style="height: 123px">
 | 
			
		||||
          <div class="card-body more-padding">
 | 
			
		||||
            <app-node-statistics [statistics$]="statistics$"></app-node-statistics>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="main-title">
 | 
			
		||||
        <span i18n="lightning.statistics-title">Channels Statistics</span> 
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="card-wrapper">
 | 
			
		||||
        
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="card">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <h5 class="card-title">Top Capacity Nodes</h5>
 | 
			
		||||
          <app-nodes-list [nodes$]="nodesByCapacity$"></app-nodes-list>
 | 
			
		||||
          <div><a [routerLink]="['/lightning/nodes' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="card">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <h5 class="card-title">Most Connected Nodes</h5>
 | 
			
		||||
          <app-nodes-list [nodes$]="nodesByChannels$"></app-nodes-list>
 | 
			
		||||
          <div><a [routerLink]="['/lightning/nodes' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,80 @@
 | 
			
		||||
.dashboard-container {
 | 
			
		||||
  padding-bottom: 60px;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  margin-top: 0.5rem;
 | 
			
		||||
  @media (min-width: 992px) {
 | 
			
		||||
    padding-bottom: 0px;
 | 
			
		||||
  }
 | 
			
		||||
  .col {
 | 
			
		||||
    margin-bottom: 1.5rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card {
 | 
			
		||||
  background-color: #1d1f31;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-title {
 | 
			
		||||
  font-size: 1rem;
 | 
			
		||||
  color: #4a68b9;
 | 
			
		||||
}
 | 
			
		||||
.card-title > a {
 | 
			
		||||
  color: #4a68b9;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-body {
 | 
			
		||||
  padding: 1.25rem 1rem 0.75rem 1rem;
 | 
			
		||||
}
 | 
			
		||||
.card-body.pool-ranking {
 | 
			
		||||
  padding: 1.25rem 0.25rem 0.75rem 0.25rem;
 | 
			
		||||
}
 | 
			
		||||
.card-text {
 | 
			
		||||
  font-size: 22px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.main-title {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  color: #ffffff91;
 | 
			
		||||
  margin-top: -13px;
 | 
			
		||||
  font-size: 10px;
 | 
			
		||||
  text-transform: uppercase;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  padding-bottom: 3px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.more-padding {
 | 
			
		||||
  padding: 18px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-wrapper {
 | 
			
		||||
  .card {
 | 
			
		||||
    height: auto !important;
 | 
			
		||||
  }
 | 
			
		||||
  .card-body {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex: inherit;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    justify-content: space-around;
 | 
			
		||||
    padding: 22px 20px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.skeleton-loader {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  display: block;
 | 
			
		||||
  &:first-child {
 | 
			
		||||
    max-width: 90px;
 | 
			
		||||
    margin: 15px auto 3px;
 | 
			
		||||
  }
 | 
			
		||||
  &:last-child {
 | 
			
		||||
    margin: 10px auto 3px;
 | 
			
		||||
    max-width: 55px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-text {
 | 
			
		||||
  font-size: 22px;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,37 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { map, share } from 'rxjs/operators';
 | 
			
		||||
import { LightningApiService } from '../lightning-api.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-lightning-dashboard',
 | 
			
		||||
  templateUrl: './lightning-dashboard.component.html',
 | 
			
		||||
  styleUrls: ['./lightning-dashboard.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class LightningDashboardComponent implements OnInit {
 | 
			
		||||
  nodesByCapacity$: Observable<any>;
 | 
			
		||||
  nodesByChannels$: Observable<any>;
 | 
			
		||||
  statistics$: Observable<any>;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private lightningApiService: LightningApiService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    const sharedObservable = this.lightningApiService.listTopNodes$().pipe(share());
 | 
			
		||||
 | 
			
		||||
    this.nodesByCapacity$ = sharedObservable
 | 
			
		||||
      .pipe(
 | 
			
		||||
        map((object) => object.topByCapacity),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.nodesByChannels$ = sharedObservable
 | 
			
		||||
      .pipe(
 | 
			
		||||
        map((object) => object.topByChannels),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.statistics$ = this.lightningApiService.getLatestStatistics$();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								frontend/src/app/lightning/lightning.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								frontend/src/app/lightning/lightning.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { CommonModule } from '@angular/common';
 | 
			
		||||
import { SharedModule } from '../shared/shared.module';
 | 
			
		||||
import { LightningDashboardComponent } from './lightning-dashboard/lightning-dashboard.component';
 | 
			
		||||
import { LightningApiService } from './lightning-api.service';
 | 
			
		||||
import { NodesListComponent } from './nodes-list/nodes-list.component';
 | 
			
		||||
import { RouterModule } from '@angular/router';
 | 
			
		||||
import { NodeStatisticsComponent } from './node-statistics/node-statistics.component';
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
    LightningDashboardComponent,
 | 
			
		||||
    NodesListComponent,
 | 
			
		||||
    NodeStatisticsComponent,
 | 
			
		||||
  ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    CommonModule,
 | 
			
		||||
    SharedModule,
 | 
			
		||||
    RouterModule,
 | 
			
		||||
  ],
 | 
			
		||||
  providers: [
 | 
			
		||||
    LightningApiService,
 | 
			
		||||
  ]
 | 
			
		||||
})
 | 
			
		||||
export class LightningModule { }
 | 
			
		||||
@ -0,0 +1,64 @@
 | 
			
		||||
<div class="fee-estimation-wrapper" *ngIf="statistics$ | async as statistics; else loadingReward">
 | 
			
		||||
  <div class="fee-estimation-container">
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title" i18n="mining.rewards">Nodes</h5>
 | 
			
		||||
      <div class="card-text" i18n-ngbTooltip="mining.rewards-desc"
 | 
			
		||||
        ngbTooltip="Amount being paid to miners in the past 144 blocks" placement="bottom">
 | 
			
		||||
        <div class="fee-text">
 | 
			
		||||
          {{ statistics.latest.node_count | number }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <span class="fiat">
 | 
			
		||||
          <app-change [current]="statistics.latest.node_count" [previous]="statistics.previous.node_count"></app-change>
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title" i18n="mining.rewards-per-tx">Channels</h5>
 | 
			
		||||
      <div class="card-text" i18n-ngbTooltip="mining.rewards-per-tx-desc"
 | 
			
		||||
        ngbTooltip="Average miners' reward per transaction in the past 144 blocks" placement="bottom">
 | 
			
		||||
        <div class="fee-text">
 | 
			
		||||
          {{ statistics.latest.channel_count | number }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <span class="fiat">
 | 
			
		||||
          <app-change [current]="statistics.latest.channel_count" [previous]="statistics.previous.channel_count"></app-change>
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title" i18n="mining.average-fee">Average Channel</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.average_channel_size" digitsInfo="1.2-3"></app-amount>
 | 
			
		||||
        <span class="fiat">
 | 
			
		||||
          <app-change [current]="statistics.latest.average_channel_size" [previous]="statistics.previous.average_channel_size"></app-change>
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<ng-template #loadingReward>
 | 
			
		||||
  <div class="fee-estimation-container loading-container">
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title" i18n="mining.rewards">Nodes</h5>
 | 
			
		||||
      <div class="card-text">
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title" i18n="mining.rewards-per-tx">Channels</h5>
 | 
			
		||||
      <div class="card-text">
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title" i18n="mining.average-fee">Average Channel</h5>
 | 
			
		||||
      <div class="card-text">
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
        <div class="skeleton-loader"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -0,0 +1,85 @@
 | 
			
		||||
.card-title {
 | 
			
		||||
  color: #4a68b9;
 | 
			
		||||
  font-size: 10px;
 | 
			
		||||
  margin-bottom: 4px;  
 | 
			
		||||
  font-size: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-text {
 | 
			
		||||
  font-size: 22px;
 | 
			
		||||
  span {
 | 
			
		||||
    font-size: 11px;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    top: -2px;
 | 
			
		||||
    display: inline-flex;
 | 
			
		||||
  }
 | 
			
		||||
  .green-color {
 | 
			
		||||
    display: block;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fee-estimation-container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  @media (min-width: 376px) {
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
  }  
 | 
			
		||||
  .item {
 | 
			
		||||
    max-width: 150px;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    width: -webkit-fill-available;
 | 
			
		||||
    @media (min-width: 376px) {
 | 
			
		||||
      margin: 0 auto 0px;
 | 
			
		||||
    }    
 | 
			
		||||
    &:first-child{
 | 
			
		||||
      display: none;
 | 
			
		||||
      @media (min-width: 485px) {
 | 
			
		||||
        display: block;
 | 
			
		||||
      }    
 | 
			
		||||
      @media (min-width: 768px) {
 | 
			
		||||
        display: none;
 | 
			
		||||
      }    
 | 
			
		||||
      @media (min-width: 992px) {
 | 
			
		||||
        display: block;
 | 
			
		||||
      }    
 | 
			
		||||
    }
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
    .card-text span {
 | 
			
		||||
      color: #ffffff66;
 | 
			
		||||
      font-size: 12px;
 | 
			
		||||
      top: 0px;
 | 
			
		||||
    }
 | 
			
		||||
    .fee-text{
 | 
			
		||||
      border-bottom: 1px solid #ffffff1c;
 | 
			
		||||
      width: fit-content;
 | 
			
		||||
      margin: auto;
 | 
			
		||||
      line-height: 1.45;
 | 
			
		||||
      padding: 0px 2px;
 | 
			
		||||
    }
 | 
			
		||||
    .fiat {
 | 
			
		||||
      display: block;
 | 
			
		||||
      font-size: 14px !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.loading-container {
 | 
			
		||||
  min-height: 76px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-text {
 | 
			
		||||
  .skeleton-loader {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    display: block;
 | 
			
		||||
    &:first-child {
 | 
			
		||||
      max-width: 90px;
 | 
			
		||||
      margin: 15px auto 3px;
 | 
			
		||||
    }
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      margin: 10px auto 3px;
 | 
			
		||||
      max-width: 55px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,18 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-node-statistics',
 | 
			
		||||
  templateUrl: './node-statistics.component.html',
 | 
			
		||||
  styleUrls: ['./node-statistics.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class NodeStatisticsComponent implements OnInit {
 | 
			
		||||
  @Input() statistics$: Observable<any>;
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,39 @@
 | 
			
		||||
<div style="min-height: 295px">
 | 
			
		||||
 | 
			
		||||
  <table class="table table-borderless">
 | 
			
		||||
    <thead>
 | 
			
		||||
      <th class="alias text-left" i18n="nodes.alias">Alias</th>
 | 
			
		||||
      <th class="capacity text-right" i18n="node.capacity">Capacity</th>
 | 
			
		||||
      <th class="channels text-right" i18n="node.channels">Channels</th>
 | 
			
		||||
    </thead>
 | 
			
		||||
    <tbody *ngIf="nodes$ | async as nodes; else skeleton">
 | 
			
		||||
      <tr *ngFor="let node of nodes; let i = index;">
 | 
			
		||||
        <td class="alias text-left">
 | 
			
		||||
          <a [routerLink]="['/lightning/nodes/:public_key' | relativeUrl, node.public]">{{ node.alias }}</a>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td class="capacity text-right">
 | 
			
		||||
          <app-amount [satoshis]="node.capacity_left + node.capacity_right" digitsInfo="1.2-2"></app-amount>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td class="channels text-right">
 | 
			
		||||
          {{ node.channels_left + node.channels_right | number }}
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </tbody>
 | 
			
		||||
    <ng-template #skeleton>
 | 
			
		||||
      <tbody>
 | 
			
		||||
        <tr *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
 | 
			
		||||
          <td class="alias text-left">
 | 
			
		||||
            <span class="skeleton-loader"></span>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="capacity text-right">
 | 
			
		||||
            <span class="skeleton-loader"></span>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="channels text-right">
 | 
			
		||||
            <span class="skeleton-loader"></span>
 | 
			
		||||
          </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
      </tbody>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
  </table>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,18 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-nodes-list',
 | 
			
		||||
  templateUrl: './nodes-list.component.html',
 | 
			
		||||
  styleUrls: ['./nodes-list.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class NodesListComponent implements OnInit {
 | 
			
		||||
  @Input() nodes$: Observable<any>;
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -37,6 +37,7 @@ export interface Env {
 | 
			
		||||
  LIQUID_WEBSITE_URL: string;
 | 
			
		||||
  BISQ_WEBSITE_URL: string;
 | 
			
		||||
  MINING_DASHBOARD: boolean;
 | 
			
		||||
  LIGHTNING: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultEnv: Env = {
 | 
			
		||||
@ -60,7 +61,8 @@ const defaultEnv: Env = {
 | 
			
		||||
  'MEMPOOL_WEBSITE_URL': 'https://mempool.space',
 | 
			
		||||
  'LIQUID_WEBSITE_URL': 'https://liquid.network',
 | 
			
		||||
  'BISQ_WEBSITE_URL': 'https://bisq.markets',
 | 
			
		||||
  'MINING_DASHBOARD': true
 | 
			
		||||
  'MINING_DASHBOARD': true,
 | 
			
		||||
  'LIGHTNING': false,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,6 @@ import { BlockchainBlocksComponent } from '../components/blockchain-blocks/block
 | 
			
		||||
import { AmountComponent } from '../components/amount/amount.component';
 | 
			
		||||
import { RouterModule } from '@angular/router';
 | 
			
		||||
import { CapAddressPipe } from './pipes/cap-address-pipe/cap-address-pipe';
 | 
			
		||||
 | 
			
		||||
import { StartComponent } from '../components/start/start.component';
 | 
			
		||||
import { TransactionComponent } from '../components/transaction/transaction.component';
 | 
			
		||||
import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component';
 | 
			
		||||
@ -74,6 +73,7 @@ import { DataCyDirective } from '../data-cy.directive';
 | 
			
		||||
import { LoadingIndicatorComponent } from '../components/loading-indicator/loading-indicator.component';
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
@ -104,7 +104,6 @@ import { SvgImagesComponent } from '../components/svg-images/svg-images.componen
 | 
			
		||||
    MempoolBlocksComponent,
 | 
			
		||||
    BlockchainBlocksComponent,
 | 
			
		||||
    AmountComponent,
 | 
			
		||||
 | 
			
		||||
    AboutComponent,
 | 
			
		||||
    MasterPageComponent,
 | 
			
		||||
    BisqMasterPageComponent,
 | 
			
		||||
@ -142,6 +141,7 @@ import { SvgImagesComponent } from '../components/svg-images/svg-images.componen
 | 
			
		||||
    LoadingIndicatorComponent,
 | 
			
		||||
    IndexingProgressComponent,
 | 
			
		||||
    SvgImagesComponent,
 | 
			
		||||
    ChangeComponent,
 | 
			
		||||
  ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    CommonModule,
 | 
			
		||||
@ -163,6 +163,7 @@ import { SvgImagesComponent } from '../components/svg-images/svg-images.componen
 | 
			
		||||
    NoSanitizePipe,
 | 
			
		||||
    ShortenStringPipe,
 | 
			
		||||
    CapAddressPipe,
 | 
			
		||||
    AmountShortenerPipe,
 | 
			
		||||
  ],
 | 
			
		||||
  exports: [
 | 
			
		||||
    RouterModule,
 | 
			
		||||
@ -203,7 +204,6 @@ import { SvgImagesComponent } from '../components/svg-images/svg-images.componen
 | 
			
		||||
    MempoolBlocksComponent,
 | 
			
		||||
    BlockchainBlocksComponent,
 | 
			
		||||
    AmountComponent,
 | 
			
		||||
 | 
			
		||||
    StartComponent,
 | 
			
		||||
    TransactionComponent,
 | 
			
		||||
    BlockComponent,
 | 
			
		||||
@ -237,6 +237,7 @@ import { SvgImagesComponent } from '../components/svg-images/svg-images.componen
 | 
			
		||||
    LoadingIndicatorComponent,
 | 
			
		||||
    IndexingProgressComponent,
 | 
			
		||||
    SvgImagesComponent,
 | 
			
		||||
    ChangeComponent,
 | 
			
		||||
  ]
 | 
			
		||||
})
 | 
			
		||||
export class SharedModule {
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,8 @@
 | 
			
		||||
  "MEMPOOL": {
 | 
			
		||||
    "NETWORK": "mainnet",
 | 
			
		||||
    "BACKEND": "lnd",
 | 
			
		||||
    "HTTP_PORT": 8999,
 | 
			
		||||
    "HTTP_PORT": 8899,
 | 
			
		||||
    "API_URL_PREFIX": "/api/v1/",
 | 
			
		||||
    "STDOUT_LOG_MIN_PRIORITY": "debug"
 | 
			
		||||
  },
 | 
			
		||||
  "SYSLOG": {
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import { AbstractLightningApi } from './lightning-api-abstract-factory';
 | 
			
		||||
import LndApi from './lnd/lnd-api';
 | 
			
		||||
 | 
			
		||||
@ -2,8 +2,8 @@ import { AbstractLightningApi } from '../lightning-api-abstract-factory';
 | 
			
		||||
import { ILightningApi } from '../lightning-api.interface';
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import * as lnService from 'ln-service';
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import logger from '../../logger';
 | 
			
		||||
import config from '../../../config';
 | 
			
		||||
import logger from '../../../logger';
 | 
			
		||||
 | 
			
		||||
class LndApi implements AbstractLightningApi {
 | 
			
		||||
  private lnd: any;
 | 
			
		||||
							
								
								
									
										42
									
								
								lightning-backend/src/api/nodes/nodes.api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								lightning-backend/src/api/nodes/nodes.api.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
			
		||||
import logger from '../../logger';
 | 
			
		||||
import DB from '../../database';
 | 
			
		||||
 | 
			
		||||
class NodesApi {
 | 
			
		||||
  public async $getTopCapacityNodes(): Promise<any> {
 | 
			
		||||
    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 [rows]: any = await DB.query(query);
 | 
			
		||||
      return rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$getTopCapacityNodes error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getTopChannelsNodes(): Promise<any> {
 | 
			
		||||
    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 [rows]: any = await DB.query(query);
 | 
			
		||||
      return rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$getTopChannelsNodes error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getLatestStatistics(): Promise<any> {
 | 
			
		||||
    try {
 | 
			
		||||
      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`);
 | 
			
		||||
      return {
 | 
			
		||||
        latest: rows[0],
 | 
			
		||||
        previous: rows2[0],
 | 
			
		||||
      };
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$getTopChannelsNodes error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new NodesApi();
 | 
			
		||||
							
								
								
									
										35
									
								
								lightning-backend/src/api/nodes/nodes.routes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								lightning-backend/src/api/nodes/nodes.routes.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import { Express, Request, Response } from 'express';
 | 
			
		||||
import nodesApi from './nodes.api';
 | 
			
		||||
class NodesRoutes {
 | 
			
		||||
  constructor(app: Express) {
 | 
			
		||||
    app
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/latest', this.$getGeneralStats)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/top', this.$getTopNodes)
 | 
			
		||||
    ;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getGeneralStats(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const statistics = await nodesApi.$getLatestStatistics();
 | 
			
		||||
      res.json(statistics);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getTopNodes(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const topCapacityNodes = await nodesApi.$getTopCapacityNodes();
 | 
			
		||||
      const topChannelsNodes = await nodesApi.$getTopChannelsNodes();
 | 
			
		||||
      res.json({
 | 
			
		||||
        topByCapacity: topCapacityNodes,
 | 
			
		||||
        topByChannels: topChannelsNodes,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default NodesRoutes;
 | 
			
		||||
@ -5,6 +5,7 @@ interface IConfig {
 | 
			
		||||
    NETWORK: 'mainnet' | 'testnet' | 'signet';
 | 
			
		||||
    BACKEND: 'lnd' | 'cln' | 'ldk';
 | 
			
		||||
    HTTP_PORT: number;
 | 
			
		||||
    API_URL_PREFIX: string;
 | 
			
		||||
    STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
 | 
			
		||||
  };
 | 
			
		||||
  SYSLOG: {
 | 
			
		||||
@ -33,6 +34,7 @@ const defaults: IConfig = {
 | 
			
		||||
    'NETWORK': 'mainnet',
 | 
			
		||||
    'BACKEND': 'lnd',
 | 
			
		||||
    'HTTP_PORT': 8999,
 | 
			
		||||
    'API_URL_PREFIX': '/api/v1/',
 | 
			
		||||
    'STDOUT_LOG_MIN_PRIORITY': 'debug',
 | 
			
		||||
  },
 | 
			
		||||
  'SYSLOG': {
 | 
			
		||||
 | 
			
		||||
@ -76,6 +76,7 @@ class DatabaseMigration {
 | 
			
		||||
      await this.$executeQuery(this.getCreateStatisticsQuery(), await this.$checkIfTableExists('statistics'));
 | 
			
		||||
      await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
 | 
			
		||||
      await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
 | 
			
		||||
      await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('nodes_stats'));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
@ -204,28 +205,45 @@ class DatabaseMigration {
 | 
			
		||||
  private getCreateChannelsQuery(): string {
 | 
			
		||||
    return `CREATE TABLE IF NOT EXISTS channels (
 | 
			
		||||
      id varchar(15) NOT NULL,
 | 
			
		||||
      capacity double unsigned NOT NULL,
 | 
			
		||||
      capacity bigint(20) unsigned NOT NULL,
 | 
			
		||||
      transaction_id varchar(64) NOT NULL,
 | 
			
		||||
      transaction_vout int(11) NOT NULL,
 | 
			
		||||
      updated_at datetime NOT NULL,
 | 
			
		||||
      updated_at datetime DEFAULT NULL,
 | 
			
		||||
      node1_public_key varchar(66) NOT NULL,
 | 
			
		||||
      node1_base_fee_mtokens double unsigned NULL,
 | 
			
		||||
      node1_cltv_delta int(11) NULL,
 | 
			
		||||
      node1_fee_rate int(11) NULL,
 | 
			
		||||
      node1_is_disabled boolean NULL,
 | 
			
		||||
      node1_max_htlc_mtokens double unsigned NULL,
 | 
			
		||||
      node1_min_htlc_mtokens double unsigned NULL,
 | 
			
		||||
      node1_updated_at datetime NULL,
 | 
			
		||||
      node1_base_fee_mtokens bigint(20) unsigned DEFAULT NULL,
 | 
			
		||||
      node1_cltv_delta int(11) DEFAULT NULL,
 | 
			
		||||
      node1_fee_rate bigint(11) DEFAULT NULL,
 | 
			
		||||
      node1_is_disabled tinyint(1) DEFAULT NULL,
 | 
			
		||||
      node1_max_htlc_mtokens bigint(20) unsigned DEFAULT NULL,
 | 
			
		||||
      node1_min_htlc_mtokens bigint(20) unsigned DEFAULT NULL,
 | 
			
		||||
      node1_updated_at datetime DEFAULT NULL,
 | 
			
		||||
      node2_public_key varchar(66) NOT NULL,
 | 
			
		||||
      node2_base_fee_mtokens double unsigned NULL,
 | 
			
		||||
      node2_cltv_delta int(11) NULL,
 | 
			
		||||
      node2_fee_rate int(11) NULL,
 | 
			
		||||
      node2_is_disabled boolean NULL,
 | 
			
		||||
      node2_max_htlc_mtokens double unsigned NULL,
 | 
			
		||||
      node2_min_htlc_mtokens double unsigned NULL,
 | 
			
		||||
      node2_updated_at datetime NULL,
 | 
			
		||||
      CONSTRAINT PRIMARY KEY (id)
 | 
			
		||||
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
      node2_base_fee_mtokens bigint(20) unsigned DEFAULT NULL,
 | 
			
		||||
      node2_cltv_delta int(11) DEFAULT NULL,
 | 
			
		||||
      node2_fee_rate bigint(11) DEFAULT NULL,
 | 
			
		||||
      node2_is_disabled tinyint(1) DEFAULT NULL,
 | 
			
		||||
      node2_max_htlc_mtokens bigint(20) unsigned DEFAULT NULL,
 | 
			
		||||
      node2_min_htlc_mtokens bigint(20) unsigned DEFAULT NULL,
 | 
			
		||||
      node2_updated_at datetime DEFAULT NULL,
 | 
			
		||||
      PRIMARY KEY (id),
 | 
			
		||||
      KEY node1_public_key (node1_public_key),
 | 
			
		||||
      KEY node2_public_key (node2_public_key),
 | 
			
		||||
      KEY node1_public_key_2 (node1_public_key,node2_public_key)
 | 
			
		||||
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getCreateNodesStatsQuery(): string {
 | 
			
		||||
    return `CREATE TABLE nodes_stats (
 | 
			
		||||
      id int(11) unsigned NOT NULL AUTO_INCREMENT,
 | 
			
		||||
      public_key varchar(66) NOT NULL DEFAULT '',
 | 
			
		||||
      added date NOT NULL,
 | 
			
		||||
      capacity_left bigint(11) unsigned DEFAULT NULL,
 | 
			
		||||
      capacity_right bigint(11) unsigned DEFAULT NULL,
 | 
			
		||||
      channels_left int(11) unsigned DEFAULT NULL,
 | 
			
		||||
      channels_right int(11) unsigned DEFAULT NULL,
 | 
			
		||||
      PRIMARY KEY (id),
 | 
			
		||||
      KEY public_key (public_key)
 | 
			
		||||
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,20 @@
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
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();
 | 
			
		||||
  }
 | 
			
		||||
@ -18,6 +25,27 @@ 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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
 | 
			
		||||
import DB from '../database';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import lightningApi from '../api/lightning-api-factory';
 | 
			
		||||
import { ILightningApi } from '../api/lightning-api.interface';
 | 
			
		||||
import lightningApi from '../api/lightning/lightning-api-factory';
 | 
			
		||||
import { ILightningApi } from '../api/lightning/lightning-api.interface';
 | 
			
		||||
 | 
			
		||||
class NodeSyncService {
 | 
			
		||||
  constructor() {}
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
 | 
			
		||||
import DB from '../database';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import lightningApi from '../api/lightning-api-factory';
 | 
			
		||||
import lightningApi from '../api/lightning/lightning-api-factory';
 | 
			
		||||
 | 
			
		||||
class LightningStatsUpdater {
 | 
			
		||||
  constructor() {}
 | 
			
		||||
@ -19,12 +19,25 @@ class LightningStatsUpdater {
 | 
			
		||||
        this.$logLightningStats();
 | 
			
		||||
      }, 1000 * 60 * 60);
 | 
			
		||||
    }, difference);
 | 
			
		||||
 | 
			
		||||
    // this.$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 [nodes]: any = await DB.query(query);
 | 
			
		||||
 | 
			
		||||
    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]);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $logLightningStats() {
 | 
			
		||||
    const networkInfo = await lightningApi.$getNetworkInfo();
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const networkInfo = await lightningApi.$getNetworkInfo();
 | 
			
		||||
 | 
			
		||||
      const query = `INSERT INTO statistics(
 | 
			
		||||
          added,
 | 
			
		||||
          channel_count,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user