Nodes per ISP list component
This commit is contained in:
		
							parent
							
								
									93e93d44f4
								
							
						
					
					
						commit
						dbf60dd4d9
					
				@ -96,7 +96,7 @@ class NodesApi {
 | 
			
		||||
 | 
			
		||||
  public async $getNodesAsShare() {
 | 
			
		||||
    try {
 | 
			
		||||
      let query = `SELECT names, COUNT(DISTINCT nodes.public_key) as nodesCount, SUM(capacity) as capacity
 | 
			
		||||
      let query = `SELECT nodes.as_number as ispId, geo_names.names as names, COUNT(DISTINCT nodes.public_key) as nodesCount, SUM(capacity) as capacity
 | 
			
		||||
        FROM nodes
 | 
			
		||||
        JOIN geo_names ON geo_names.id = nodes.as_number
 | 
			
		||||
        JOIN channels ON channels.node1_public_key = nodes.public_key OR channels.node2_public_key = nodes.public_key
 | 
			
		||||
@ -111,6 +111,7 @@ class NodesApi {
 | 
			
		||||
      const nodesPerAs: any[] = [];
 | 
			
		||||
      for (const as of nodesCountPerAS) {
 | 
			
		||||
        nodesPerAs.push({
 | 
			
		||||
          ispId: as.ispId,
 | 
			
		||||
          name: JSON.parse(as.names),
 | 
			
		||||
          count: as.nodesCount,
 | 
			
		||||
          share: Math.floor(as.nodesCount / nodesWithAS[0].total * 10000) / 100,
 | 
			
		||||
@ -154,6 +155,37 @@ class NodesApi {
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getNodesPerISP(ISPId: string) {
 | 
			
		||||
    try {
 | 
			
		||||
      const query = `
 | 
			
		||||
        SELECT node_stats.public_key, node_stats.capacity, node_stats.channels, nodes.alias,
 | 
			
		||||
          UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
 | 
			
		||||
          geo_names_city.names as city, geo_names_country.names as country
 | 
			
		||||
        FROM node_stats
 | 
			
		||||
        JOIN (
 | 
			
		||||
          SELECT public_key, MAX(added) as last_added
 | 
			
		||||
          FROM node_stats
 | 
			
		||||
          GROUP BY public_key
 | 
			
		||||
        ) as b ON b.public_key = node_stats.public_key AND b.last_added = node_stats.added
 | 
			
		||||
        JOIN nodes ON nodes.public_key = node_stats.public_key
 | 
			
		||||
        JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
 | 
			
		||||
        LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
 | 
			
		||||
        WHERE nodes.as_number = ?
 | 
			
		||||
        ORDER BY capacity DESC
 | 
			
		||||
      `;
 | 
			
		||||
 | 
			
		||||
      const [rows]: any = await DB.query(query, [ISPId]);
 | 
			
		||||
      for (let i = 0; i < rows.length; ++i) {
 | 
			
		||||
        rows[i].country = JSON.parse(rows[i].country);
 | 
			
		||||
        rows[i].city = JSON.parse(rows[i].city);
 | 
			
		||||
      }
 | 
			
		||||
      return rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Cannot get nodes for ISP id ${ISPId}. Reason: ${e instanceof Error ? e.message : e}`);
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new NodesApi();
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ class NodesRoutes {
 | 
			
		||||
  public initRoutes(app: Application) {
 | 
			
		||||
    app
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/country/:country', this.$getNodesPerCountry)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp/:isp', this.$getNodesPerISP)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/search/:search', this.$searchNode)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/top', this.$getTopNodes)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/asShare', this.$getNodesAsShare)
 | 
			
		||||
@ -100,6 +101,33 @@ class NodesRoutes {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getNodesPerISP(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const [isp]: any[] = await DB.query(
 | 
			
		||||
        `SELECT geo_names.names as isp_name
 | 
			
		||||
        FROM geo_names
 | 
			
		||||
        WHERE geo_names.type = 'as_organization' AND geo_names.id = ?`,
 | 
			
		||||
        [req.params.isp]
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (isp.length === 0) {
 | 
			
		||||
        res.status(404).send(`This ISP does not exist or does not host any lightning nodes on clearnet`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const nodes = await nodesApi.$getNodesPerISP(req.params.isp);
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json({
 | 
			
		||||
        isp: JSON.parse(isp[0].isp_name),
 | 
			
		||||
        nodes: nodes,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new NodesRoutes();
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@ import { NodesNetworksChartComponent } from './nodes-networks-chart/nodes-networ
 | 
			
		||||
import { ChannelsStatisticsComponent } from './channels-statistics/channels-statistics.component';
 | 
			
		||||
import { NodesPerAsChartComponent } from '../lightning/nodes-per-as-chart/nodes-per-as-chart.component';
 | 
			
		||||
import { NodesPerCountry } from './nodes-per-country/nodes-per-country.component';
 | 
			
		||||
import { NodesPerISP } from './nodes-per-isp/nodes-per-isp.component';
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
    LightningDashboardComponent,
 | 
			
		||||
@ -37,6 +38,7 @@ import { NodesPerCountry } from './nodes-per-country/nodes-per-country.component
 | 
			
		||||
    ChannelsStatisticsComponent,
 | 
			
		||||
    NodesPerAsChartComponent,
 | 
			
		||||
    NodesPerCountry,
 | 
			
		||||
    NodesPerISP,
 | 
			
		||||
  ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    CommonModule,
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@ import { LightningWrapperComponent } from './lightning-wrapper/lightning-wrapper
 | 
			
		||||
import { NodeComponent } from './node/node.component';
 | 
			
		||||
import { ChannelComponent } from './channel/channel.component';
 | 
			
		||||
import { NodesPerCountry } from './nodes-per-country/nodes-per-country.component';
 | 
			
		||||
import { NodesPerISP } from './nodes-per-isp/nodes-per-isp.component';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
@ -27,6 +28,10 @@ const routes: Routes = [
 | 
			
		||||
          path: 'nodes/country/:country',
 | 
			
		||||
          component: NodesPerCountry,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          path: 'nodes/isp/:isp',
 | 
			
		||||
          component: NodesPerISP,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          path: '**',
 | 
			
		||||
          redirectTo: ''
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,7 @@
 | 
			
		||||
      <thead>
 | 
			
		||||
        <tr>
 | 
			
		||||
          <th *ngIf="!isMobile()" i18n="mining.rank">Rank</th>
 | 
			
		||||
          <th i18n="lightning.as-name">Name</th>
 | 
			
		||||
          <th i18n="lightning.isp">ISP</th>
 | 
			
		||||
          <th *ngIf="!isMobile()" i18n="lightning.share">Share</th>
 | 
			
		||||
          <th i18n="lightning.nodes-count">Nodes</th>
 | 
			
		||||
          <th i18n="lightning.capacity">Capacity</th>
 | 
			
		||||
@ -34,7 +34,9 @@
 | 
			
		||||
      <tbody [attr.data-cy]="'pools-table'" *ngIf="(nodesPerAsObservable$ | async) as asList">
 | 
			
		||||
        <tr *ngFor="let asEntry of asList">
 | 
			
		||||
          <td *ngIf="!isMobile()">{{ asEntry.rank }}</td>
 | 
			
		||||
          <td class="text-truncate" style="max-width: 100px">{{ asEntry.name }}</td>
 | 
			
		||||
          <td class="text-truncate" style="max-width: 100px">
 | 
			
		||||
            <a [routerLink]="[('/lightning/nodes/isp/' + asEntry.ispId) | relativeUrl]">{{ asEntry.name }}</a>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td *ngIf="!isMobile()">{{ asEntry.share }}%</td>
 | 
			
		||||
          <td>{{ asEntry.count }}</td>
 | 
			
		||||
          <td><app-amount [satoshis]="asEntry.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount></td>
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,14 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, OnInit, HostBinding } from '@angular/core';
 | 
			
		||||
import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone } from '@angular/core';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
import { EChartsOption, PieSeriesOption } from 'echarts';
 | 
			
		||||
import { map, Observable, share, tap } from 'rxjs';
 | 
			
		||||
import { chartColors } from 'src/app/app.constants';
 | 
			
		||||
import { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
import { StateService } from 'src/app/services/state.service';
 | 
			
		||||
import { download } from 'src/app/shared/graphs.utils';
 | 
			
		||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
 | 
			
		||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-nodes-per-as-chart',
 | 
			
		||||
@ -31,7 +34,10 @@ export class NodesPerAsChartComponent implements OnInit {
 | 
			
		||||
  constructor(
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
    private amountShortenerPipe: AmountShortenerPipe
 | 
			
		||||
    private amountShortenerPipe: AmountShortenerPipe,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    private zone: NgZone,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
  ) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -96,7 +102,7 @@ export class NodesPerAsChartComponent implements OnInit {
 | 
			
		||||
            ;
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        data: as.slug,
 | 
			
		||||
        data: as.ispId,
 | 
			
		||||
      } as PieSeriesOption);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -126,6 +132,7 @@ export class NodesPerAsChartComponent implements OnInit {
 | 
			
		||||
            totalNodeOther.toString() + ` nodes`;
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      data: 9999 as any,
 | 
			
		||||
    } as PieSeriesOption);
 | 
			
		||||
 | 
			
		||||
    return data;
 | 
			
		||||
@ -149,7 +156,7 @@ export class NodesPerAsChartComponent implements OnInit {
 | 
			
		||||
        {
 | 
			
		||||
          zlevel: 0,
 | 
			
		||||
          minShowLabelAngle: 3.6,
 | 
			
		||||
          name: 'Mining pool',
 | 
			
		||||
          name: 'Lightning nodes',
 | 
			
		||||
          type: 'pie',
 | 
			
		||||
          radius: pieSize,
 | 
			
		||||
          data: this.generateChartSerieData(as),
 | 
			
		||||
@ -193,6 +200,16 @@ export class NodesPerAsChartComponent implements OnInit {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.chartInstance = ec;
 | 
			
		||||
 | 
			
		||||
    this.chartInstance.on('click', (e) => {
 | 
			
		||||
      if (e.data.data === 9999) { // "Other"
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      this.zone.run(() => {
 | 
			
		||||
        const url = new RelativeUrlPipe(this.stateService).transform(`/lightning/nodes/isp/${e.data.data}`);
 | 
			
		||||
        this.router.navigate([url]);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onSaveChart() {
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,42 @@
 | 
			
		||||
<div class="container-xl full-height" style="min-height: 335px">
 | 
			
		||||
  <h1 class="float-left" i18n="lightning.nodes-for-isp">Lightning nodes on ISP: {{ isp?.name }} [AS {{isp?.id}}]</h1>
 | 
			
		||||
 | 
			
		||||
  <div style="min-height: 295px">
 | 
			
		||||
    <table class="table table-borderless">
 | 
			
		||||
      <thead>
 | 
			
		||||
        <th class="alias text-left" i18n="lightning.alias">Alias</th>
 | 
			
		||||
        <th class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
 | 
			
		||||
        <th class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
 | 
			
		||||
        <th class="capacity text-right" i18n="lightning.capacity">Capacity</th>
 | 
			
		||||
        <th class="channels text-right" i18n="lightning.channels">Channels</th>
 | 
			
		||||
        <th class="city text-right" i18n="lightning.city">City</th>
 | 
			
		||||
      </thead>
 | 
			
		||||
      <tbody *ngIf="nodes$ | async as nodes">
 | 
			
		||||
        <tr *ngFor="let node of nodes; let i= index; trackBy: trackByPublicKey">
 | 
			
		||||
          <td class="alias text-left text-truncate">
 | 
			
		||||
            <a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="timestamp-first text-left">
 | 
			
		||||
            <app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.first_seen"></app-timestamp>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="timestamp-update text-left">
 | 
			
		||||
            <app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updated_at"></app-timestamp>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="capacity text-right">
 | 
			
		||||
            <app-amount *ngIf="node.capacity > 100000000; else smallchannel" [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
 | 
			
		||||
            <ng-template #smallchannel>
 | 
			
		||||
              {{ node.capacity | amountShortener: 1 }}
 | 
			
		||||
              <span class="sats" i18n="shared.sats">sats</span>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="channels text-right">
 | 
			
		||||
            {{ node.channels }}
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="city text-right text-truncate">
 | 
			
		||||
            {{ node?.city?.en ?? '-' }}
 | 
			
		||||
          </td>
 | 
			
		||||
      </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,62 @@
 | 
			
		||||
.container-xl {
 | 
			
		||||
  max-width: 1400px;
 | 
			
		||||
  padding-bottom: 100px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sats {
 | 
			
		||||
  color: #ffffff66;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  top: 0px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.alias {
 | 
			
		||||
  width: 30%;
 | 
			
		||||
  max-width: 400px;
 | 
			
		||||
  padding-right: 70px;
 | 
			
		||||
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    width: 50%;
 | 
			
		||||
    max-width: 150px;
 | 
			
		||||
    padding-right: 0px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.timestamp-first {
 | 
			
		||||
  width: 20%;
 | 
			
		||||
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    display: none
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.timestamp-update {
 | 
			
		||||
  width: 16%;
 | 
			
		||||
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    display: none
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.capacity {
 | 
			
		||||
  width: 10%;
 | 
			
		||||
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    width: 25%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.channels {
 | 
			
		||||
  width: 10%;
 | 
			
		||||
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    width: 25%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.city {
 | 
			
		||||
  max-width: 150px;
 | 
			
		||||
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    display: none
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,40 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
 | 
			
		||||
import { ActivatedRoute } from '@angular/router';
 | 
			
		||||
import { map, Observable } from 'rxjs';
 | 
			
		||||
import { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-nodes-per-isp',
 | 
			
		||||
  templateUrl: './nodes-per-isp.component.html',
 | 
			
		||||
  styleUrls: ['./nodes-per-isp.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class NodesPerISP implements OnInit {
 | 
			
		||||
  nodes$: Observable<any>;
 | 
			
		||||
  isp: {name: string, id: number};
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.nodes$ = this.apiService.getNodeForISP$(this.route.snapshot.params.isp)
 | 
			
		||||
      .pipe(
 | 
			
		||||
        map(response => {
 | 
			
		||||
          this.isp = {
 | 
			
		||||
            name: response.isp,
 | 
			
		||||
            id: this.route.snapshot.params.isp
 | 
			
		||||
          };
 | 
			
		||||
          this.seoService.setTitle($localize`Lightning nodes on ISP: ${response.isp} [AS${this.route.snapshot.params.isp}]`);
 | 
			
		||||
          return response.nodes;
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  trackByPublicKey(index: number, node: any) {
 | 
			
		||||
    return node.public_key;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -258,4 +258,8 @@ export class ApiService {
 | 
			
		||||
  getNodeForCountry$(country: string): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/country/' + country);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getNodeForISP$(isp: string): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp/' + isp);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user