Merge branch 'master' into subnet-navigation
This commit is contained in:
		
						commit
						f3cfc7f80b
					
				@ -129,6 +129,56 @@ class NodesApi {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getFeeHistogram(node_public_key: string): Promise<unknown> {
 | 
			
		||||
    try {
 | 
			
		||||
      const inQuery = `
 | 
			
		||||
        SELECT CASE WHEN fee_rate <= 10.0 THEN CEIL(fee_rate)
 | 
			
		||||
                    WHEN (fee_rate > 10.0 and fee_rate <= 100.0) THEN CEIL(fee_rate / 10.0) * 10.0
 | 
			
		||||
                    WHEN (fee_rate > 100.0 and fee_rate <= 1000.0) THEN CEIL(fee_rate / 100.0) * 100.0
 | 
			
		||||
                    WHEN fee_rate > 1000.0 THEN CEIL(fee_rate / 1000.0) * 1000.0
 | 
			
		||||
               END as bucket,
 | 
			
		||||
               count(short_id) as count,
 | 
			
		||||
               sum(capacity) as capacity
 | 
			
		||||
        FROM (
 | 
			
		||||
          SELECT CASE WHEN node1_public_key = ? THEN node2_fee_rate WHEN node2_public_key = ? THEN node1_fee_rate END as fee_rate,
 | 
			
		||||
                 short_id as short_id,
 | 
			
		||||
                 capacity as capacity
 | 
			
		||||
          FROM channels
 | 
			
		||||
          WHERE status = 1 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)
 | 
			
		||||
        ) as fee_rate_table
 | 
			
		||||
        GROUP BY bucket;
 | 
			
		||||
      `;
 | 
			
		||||
      const [inRows]: any[] = await DB.query(inQuery, [node_public_key, node_public_key, node_public_key, node_public_key]);
 | 
			
		||||
 | 
			
		||||
      const outQuery = `
 | 
			
		||||
        SELECT CASE WHEN fee_rate <= 10.0 THEN CEIL(fee_rate)
 | 
			
		||||
                    WHEN (fee_rate > 10.0 and fee_rate <= 100.0) THEN CEIL(fee_rate / 10.0) * 10.0
 | 
			
		||||
                    WHEN (fee_rate > 100.0 and fee_rate <= 1000.0) THEN CEIL(fee_rate / 100.0) * 100.0
 | 
			
		||||
                    WHEN fee_rate > 1000.0 THEN CEIL(fee_rate / 1000.0) * 1000.0
 | 
			
		||||
               END as bucket,
 | 
			
		||||
               count(short_id) as count,
 | 
			
		||||
               sum(capacity) as capacity
 | 
			
		||||
        FROM (
 | 
			
		||||
          SELECT CASE WHEN node1_public_key = ? THEN node1_fee_rate WHEN node2_public_key = ? THEN node2_fee_rate END as fee_rate,
 | 
			
		||||
                 short_id as short_id,
 | 
			
		||||
                 capacity as capacity
 | 
			
		||||
          FROM channels
 | 
			
		||||
          WHERE status = 1 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)
 | 
			
		||||
        ) as fee_rate_table
 | 
			
		||||
        GROUP BY bucket;
 | 
			
		||||
      `;
 | 
			
		||||
      const [outRows]: any[] = await DB.query(outQuery, [node_public_key, node_public_key, node_public_key, node_public_key]);
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        incoming: inRows.length > 0 ? inRows : [],
 | 
			
		||||
        outgoing: outRows.length > 0 ? outRows : [],
 | 
			
		||||
      };
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Cannot get node fee distribution for ${node_public_key}. Reason: ${(e instanceof Error ? e.message : e)}`);
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getAllNodes(): Promise<any> {
 | 
			
		||||
    try {
 | 
			
		||||
      const query = `SELECT * FROM nodes`;
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@ class NodesRoutes {
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/connectivity', this.$getTopNodesByChannels)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/age', this.$getOldestNodes)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/fees/histogram', this.$getFeeHistogram)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/group/:name', this.$getNodeGroup)
 | 
			
		||||
    ;
 | 
			
		||||
@ -95,6 +96,22 @@ class NodesRoutes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getFeeHistogram(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const node = await nodesApi.$getFeeHistogram(req.params.public_key);
 | 
			
		||||
      if (!node) {
 | 
			
		||||
        res.status(404).send('Node not found');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(node);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getNodesRanking(req: Request, res: Response): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const topCapacityNodes = await nodesApi.$getTopCapacityNodes(false);
 | 
			
		||||
 | 
			
		||||
@ -53,6 +53,10 @@ export class LightningApiService {
 | 
			
		||||
    return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/statistics');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getNodeFeeHistogram$(publicKey: string): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/fees/histogram');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getNodesRanking$(): Observable<INodesRanking> {
 | 
			
		||||
    return this.httpClient.get<INodesRanking>(this.apiBasePath + '/api/v1/lightning/nodes/rankings');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@ import { ChannelBoxComponent } from './channel/channel-box/channel-box.component
 | 
			
		||||
import { ClosingTypeComponent } from './channel/closing-type/closing-type.component';
 | 
			
		||||
import { LightningStatisticsChartComponent } from './statistics-chart/lightning-statistics-chart.component';
 | 
			
		||||
import { NodeStatisticsChartComponent } from './node-statistics-chart/node-statistics-chart.component';
 | 
			
		||||
import { NodeFeeChartComponent } from './node-fee-chart/node-fee-chart.component';
 | 
			
		||||
import { GraphsModule } from '../graphs/graphs.module';
 | 
			
		||||
import { NodesNetworksChartComponent } from './nodes-networks-chart/nodes-networks-chart.component';
 | 
			
		||||
import { ChannelsStatisticsComponent } from './channels-statistics/channels-statistics.component';
 | 
			
		||||
@ -38,6 +39,7 @@ import { GroupComponent } from './group/group.component';
 | 
			
		||||
    NodesListComponent,
 | 
			
		||||
    NodeStatisticsComponent,
 | 
			
		||||
    NodeStatisticsChartComponent,
 | 
			
		||||
    NodeFeeChartComponent,
 | 
			
		||||
    NodeComponent,
 | 
			
		||||
    ChannelsListComponent,
 | 
			
		||||
    ChannelComponent,
 | 
			
		||||
@ -73,6 +75,7 @@ import { GroupComponent } from './group/group.component';
 | 
			
		||||
    NodesListComponent,
 | 
			
		||||
    NodeStatisticsComponent,
 | 
			
		||||
    NodeStatisticsChartComponent,
 | 
			
		||||
    NodeFeeChartComponent,
 | 
			
		||||
    NodeComponent,
 | 
			
		||||
    ChannelsListComponent,
 | 
			
		||||
    ChannelComponent,
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,7 @@
 | 
			
		||||
<div class="full-container">
 | 
			
		||||
  <h2 i18n="lightning.node-fee-distribution">Fee distribution</h2>
 | 
			
		||||
  <div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
 | 
			
		||||
  <div class="text-center loadingGraphs" *ngIf="isLoading">
 | 
			
		||||
    <div class="spinner-border text-light"></div>d
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,5 @@
 | 
			
		||||
.full-container {
 | 
			
		||||
  margin-top: 25px;
 | 
			
		||||
  margin-bottom: 25px;
 | 
			
		||||
  min-height: 100%;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,265 @@
 | 
			
		||||
import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
 | 
			
		||||
import { EChartsOption } from 'echarts';
 | 
			
		||||
import { switchMap } from 'rxjs/operators';
 | 
			
		||||
import { download } from '../../shared/graphs.utils';
 | 
			
		||||
import { LightningApiService } from '../lightning-api.service';
 | 
			
		||||
import { ActivatedRoute, ParamMap } from '@angular/router';
 | 
			
		||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-node-fee-chart',
 | 
			
		||||
  templateUrl: './node-fee-chart.component.html',
 | 
			
		||||
  styleUrls: ['./node-fee-chart.component.scss'],
 | 
			
		||||
  styles: [`
 | 
			
		||||
    .loadingGraphs {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 50%;
 | 
			
		||||
      left: calc(50% - 15px);
 | 
			
		||||
      z-index: 100;
 | 
			
		||||
    }
 | 
			
		||||
  `],
 | 
			
		||||
})
 | 
			
		||||
export class NodeFeeChartComponent implements OnInit {
 | 
			
		||||
  chartOptions: EChartsOption = {};
 | 
			
		||||
  chartInitOptions = {
 | 
			
		||||
    renderer: 'svg',
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  @HostBinding('attr.dir') dir = 'ltr';
 | 
			
		||||
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
  chartInstance: any = undefined;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(LOCALE_ID) public locale: string,
 | 
			
		||||
    private lightningApiService: LightningApiService,
 | 
			
		||||
    private activatedRoute: ActivatedRoute,
 | 
			
		||||
    private amountShortenerPipe: AmountShortenerPipe,
 | 
			
		||||
  ) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
 | 
			
		||||
    this.activatedRoute.paramMap
 | 
			
		||||
      .pipe(
 | 
			
		||||
        switchMap((params: ParamMap) => {
 | 
			
		||||
          this.isLoading = true;
 | 
			
		||||
          return this.lightningApiService.getNodeFeeHistogram$(params.get('public_key'));
 | 
			
		||||
        }),
 | 
			
		||||
      ).subscribe((data) => {
 | 
			
		||||
        if (data && data.incoming && data.outgoing) {
 | 
			
		||||
          const outgoingHistogram = this.bucketsToHistogram(data.outgoing);
 | 
			
		||||
          const incomingHistogram = this.bucketsToHistogram(data.incoming);
 | 
			
		||||
          this.prepareChartOptions(outgoingHistogram, incomingHistogram);
 | 
			
		||||
        }
 | 
			
		||||
        this.isLoading = false;
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bucketsToHistogram(buckets): { label: string, count: number, capacity: number}[] {
 | 
			
		||||
    const histogram = [];
 | 
			
		||||
    let increment = 1;
 | 
			
		||||
    let lower = -increment;
 | 
			
		||||
    let upper = 0;
 | 
			
		||||
 | 
			
		||||
    let nullBucket;
 | 
			
		||||
    if (buckets.length && buckets[0] && buckets[0].bucket == null) {
 | 
			
		||||
      nullBucket = buckets.shift();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    while (upper <= 5000) {
 | 
			
		||||
      let bucket;
 | 
			
		||||
      if (buckets.length && buckets[0] && upper >= Number(buckets[0].bucket)) {
 | 
			
		||||
        bucket = buckets.shift();
 | 
			
		||||
      }
 | 
			
		||||
      histogram.push({
 | 
			
		||||
        label: upper === 0 ? '0 ppm' : `${lower} - ${upper} ppm`,
 | 
			
		||||
        count: Number(bucket?.count || 0) + (upper === 0 ? Number(nullBucket?.count || 0) : 0),
 | 
			
		||||
        capacity: Number(bucket?.capacity || 0) + (upper === 0 ? Number(nullBucket?.capacity || 0) : 0),
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (upper >= increment * 10) {
 | 
			
		||||
        increment *= 10;
 | 
			
		||||
        lower = increment;
 | 
			
		||||
        upper = increment + increment;
 | 
			
		||||
      } else {
 | 
			
		||||
        lower += increment;
 | 
			
		||||
        upper += increment;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const rest = buckets.reduce((acc, bucket) => {
 | 
			
		||||
      acc.count += Number(bucket.count);
 | 
			
		||||
      acc.capacity += Number(bucket.capacity);
 | 
			
		||||
      return acc;
 | 
			
		||||
    }, { count: 0, capacity: 0 });
 | 
			
		||||
    histogram.push({
 | 
			
		||||
      label: `5000+ ppm`,
 | 
			
		||||
      count: rest.count,
 | 
			
		||||
      capacity: rest.capacity,
 | 
			
		||||
    });
 | 
			
		||||
    return histogram;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  prepareChartOptions(outgoingData, incomingData): void {
 | 
			
		||||
    let title: object;
 | 
			
		||||
    if (outgoingData.length === 0) {
 | 
			
		||||
      title = {
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          color: 'grey',
 | 
			
		||||
          fontSize: 15
 | 
			
		||||
        },
 | 
			
		||||
        text: $localize`No data to display yet. Try again later.`,
 | 
			
		||||
        left: 'center',
 | 
			
		||||
        top: 'center'
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.chartOptions = {
 | 
			
		||||
      title: outgoingData.length === 0 ? title : undefined,
 | 
			
		||||
      animation: false,
 | 
			
		||||
      grid: {
 | 
			
		||||
        top: 30,
 | 
			
		||||
        bottom: 20,
 | 
			
		||||
        right: 20,
 | 
			
		||||
        left: 65,
 | 
			
		||||
      },
 | 
			
		||||
      tooltip: {
 | 
			
		||||
        show: !this.isMobile(),
 | 
			
		||||
        trigger: 'axis',
 | 
			
		||||
        axisPointer: {
 | 
			
		||||
          type: 'line'
 | 
			
		||||
        },
 | 
			
		||||
        backgroundColor: 'rgba(17, 19, 31, 1)',
 | 
			
		||||
        borderRadius: 4,
 | 
			
		||||
        shadowColor: 'rgba(0, 0, 0, 0.5)',
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          color: '#b1b1b1',
 | 
			
		||||
          align: 'left',
 | 
			
		||||
        },
 | 
			
		||||
        borderColor: '#000',
 | 
			
		||||
        formatter: (ticks): string => {
 | 
			
		||||
          return `
 | 
			
		||||
            <b style="color: white; margin-left: 2px">${ticks[0].data.label}</b><br>
 | 
			
		||||
            <br>
 | 
			
		||||
            <b style="color: white; margin-left: 2px">${ticks[0].marker} Outgoing</b><br>
 | 
			
		||||
            <span>Capacity: ${this.amountShortenerPipe.transform(ticks[0].data.capacity, 2, undefined, true)} sats</span><br>
 | 
			
		||||
            <span>Channels: ${ticks[0].data.count}</span><br>
 | 
			
		||||
            <br>
 | 
			
		||||
            <b style="color: white; margin-left: 2px">${ticks[1].marker} Incoming</b><br>
 | 
			
		||||
            <span>Capacity: ${this.amountShortenerPipe.transform(ticks[1].data.capacity, 2, undefined, true)} sats</span><br>
 | 
			
		||||
            <span>Channels: ${ticks[1].data.count}</span><br>
 | 
			
		||||
          `;
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      xAxis: outgoingData.length === 0 ? undefined : {
 | 
			
		||||
        type: 'category',
 | 
			
		||||
        axisLine: { onZero: true },
 | 
			
		||||
        axisLabel: {
 | 
			
		||||
          align: 'center',
 | 
			
		||||
          fontSize: 11,
 | 
			
		||||
          lineHeight: 12,
 | 
			
		||||
          hideOverlap: true,
 | 
			
		||||
          padding: [0, 5],
 | 
			
		||||
        },
 | 
			
		||||
        data: outgoingData.map(bucket => bucket.label)
 | 
			
		||||
      },
 | 
			
		||||
      legend: outgoingData.length === 0 ? undefined : {
 | 
			
		||||
        padding: 10,
 | 
			
		||||
        data: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'Outgoing Fees',
 | 
			
		||||
            inactiveColor: 'rgb(110, 112, 121)',
 | 
			
		||||
            textStyle: {
 | 
			
		||||
              color: 'white',
 | 
			
		||||
            },
 | 
			
		||||
            icon: 'roundRect',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'Incoming Fees',
 | 
			
		||||
            inactiveColor: 'rgb(110, 112, 121)',
 | 
			
		||||
            textStyle: {
 | 
			
		||||
              color: 'white',
 | 
			
		||||
            },
 | 
			
		||||
            icon: 'roundRect',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      yAxis: outgoingData.length === 0 ? undefined : [
 | 
			
		||||
        {
 | 
			
		||||
          type: 'value',
 | 
			
		||||
          axisLabel: {
 | 
			
		||||
            color: 'rgb(110, 112, 121)',
 | 
			
		||||
            formatter: (val) => {
 | 
			
		||||
              return `${this.amountShortenerPipe.transform(Math.abs(val), 2, undefined, true)} sats`;
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          splitLine: {
 | 
			
		||||
            lineStyle: {
 | 
			
		||||
              type: 'dotted',
 | 
			
		||||
              color: '#ffffff66',
 | 
			
		||||
              opacity: 0.25,
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      series: outgoingData.length === 0 ? undefined : [
 | 
			
		||||
        {
 | 
			
		||||
          zlevel: 0,
 | 
			
		||||
          name: 'Outgoing Fees',
 | 
			
		||||
          data: outgoingData.map(bucket => ({
 | 
			
		||||
            value: bucket.capacity,
 | 
			
		||||
            label: bucket.label,
 | 
			
		||||
            capacity: bucket.capacity,
 | 
			
		||||
            count: bucket.count,
 | 
			
		||||
          })),
 | 
			
		||||
          type: 'bar',
 | 
			
		||||
          barWidth: '90%',
 | 
			
		||||
          barMaxWidth: 50,
 | 
			
		||||
          stack: 'fees',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          zlevel: 0,
 | 
			
		||||
          name: 'Incoming Fees',
 | 
			
		||||
          data: incomingData.map(bucket => ({
 | 
			
		||||
            value: -bucket.capacity,
 | 
			
		||||
            label: bucket.label,
 | 
			
		||||
            capacity: bucket.capacity,
 | 
			
		||||
            count: bucket.count,
 | 
			
		||||
          })),
 | 
			
		||||
          type: 'bar',
 | 
			
		||||
          barWidth: '90%',
 | 
			
		||||
          barMaxWidth: 50,
 | 
			
		||||
          stack: 'fees',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onChartInit(ec) {
 | 
			
		||||
    if (this.chartInstance !== undefined) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.chartInstance = ec;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isMobile() {
 | 
			
		||||
    return (window.innerWidth <= 767.98);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onSaveChart() {
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    const prevBottom = this.chartOptions.grid.bottom;
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    this.chartOptions.grid.bottom = 40;
 | 
			
		||||
    this.chartOptions.backgroundColor = '#11131f';
 | 
			
		||||
    this.chartInstance.setOption(this.chartOptions);
 | 
			
		||||
    download(this.chartInstance.getDataURL({
 | 
			
		||||
      pixelRatio: 2,
 | 
			
		||||
    }), `node-fee-chart.svg`);
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    this.chartOptions.grid.bottom = prevBottom;
 | 
			
		||||
    this.chartOptions.backgroundColor = 'none';
 | 
			
		||||
    this.chartInstance.setOption(this.chartOptions);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -140,6 +140,8 @@
 | 
			
		||||
 | 
			
		||||
    <app-node-channels style="display:block;margin-bottom: 40px" [publicKey]="node.public_key"></app-node-channels>
 | 
			
		||||
 | 
			
		||||
    <app-node-fee-chart style="display:block;margin-bottom: 40px"></app-node-fee-chart>
 | 
			
		||||
 | 
			
		||||
    <div class="d-flex">
 | 
			
		||||
      <h2 *ngIf="channelsListStatus === 'open'">
 | 
			
		||||
        <span i18n="lightning.open-channels">Open channels</span>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user