diff --git a/backend/src/api/explorer/channels.api.ts b/backend/src/api/explorer/channels.api.ts index a2db61f78..a0a617e43 100644 --- a/backend/src/api/explorer/channels.api.ts +++ b/backend/src/api/explorer/channels.api.ts @@ -245,8 +245,12 @@ class ChannelsApi { let channelStatusFilter; if (status === 'open') { channelStatusFilter = '< 2'; + } else if (status === 'active') { + channelStatusFilter = '= 1'; } else if (status === 'closed') { channelStatusFilter = '= 2'; + } else { + throw new Error('getChannelsForNode: Invalid status requested'); } // Channels originating from node @@ -275,7 +279,12 @@ class ChannelsApi { allChannels.sort((a, b) => { return b.capacity - a.capacity; }); - allChannels = allChannels.slice(index, index + length); + + if (index >= 0) { + allChannels = allChannels.slice(index, index + length); + } else if (index === -1) { // Node channels tree chart + allChannels = allChannels.slice(0, 1000); + } const channels: any[] = [] for (const row of allChannels) { diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 838208cc3..66ee8179e 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -188,4 +188,4 @@ export interface IOldestNodes { updatedAt?: number, city?: any, country?: any, -} \ No newline at end of file +} diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.html b/frontend/src/app/lightning/channels-list/channels-list.component.html index 780c0fdf6..0dd2de183 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.html +++ b/frontend/src/app/lightning/channels-list/channels-list.component.html @@ -87,7 +87,7 @@ -

Channels

+

Channels

diff --git a/frontend/src/app/lightning/lightning-api.service.ts b/frontend/src/app/lightning/lightning-api.service.ts index 1963235ef..cae853df5 100644 --- a/frontend/src/app/lightning/lightning-api.service.ts +++ b/frontend/src/app/lightning/lightning-api.service.ts @@ -32,7 +32,7 @@ export class LightningApiService { } getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable { - let params = new HttpParams() + const params = new HttpParams() .set('public_key', publicKey) .set('index', index) .set('status', status) diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts index 7ca02b2ba..beb0b5c46 100644 --- a/frontend/src/app/lightning/lightning.module.ts +++ b/frontend/src/app/lightning/lightning.module.ts @@ -29,6 +29,7 @@ import { TopNodesPerChannels } from '../lightning/nodes-ranking/top-nodes-per-ch import { TopNodesPerCapacity } from '../lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component'; import { OldestNodes } from '../lightning/nodes-ranking/oldest-nodes/oldest-nodes.component'; import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component'; +import { NodeChannels } from '../lightning/nodes-channels/node-channels.component'; @NgModule({ declarations: [ @@ -56,6 +57,7 @@ import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/no TopNodesPerCapacity, OldestNodes, NodesRankingsDashboard, + NodeChannels, ], imports: [ CommonModule, @@ -89,6 +91,7 @@ import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/no TopNodesPerCapacity, OldestNodes, NodesRankingsDashboard, + NodeChannels, ], providers: [ LightningApiService, diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index e90b7d5ef..a97707930 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -118,15 +118,22 @@ - - +
+ -
-

Channels ({{ channelsListStatus === 'open' ? node.opened_channel_count : node.closed_channel_count }})

+

Node history

+ + +

Active channels map

+ + +
+

Channels ({{ channelsListStatus === 'open' ? node.opened_channel_count : node.closed_channel_count }})

+
+ +
- -
diff --git a/frontend/src/app/lightning/nodes-channels/node-channels.component.html b/frontend/src/app/lightning/nodes-channels/node-channels.component.html new file mode 100644 index 000000000..43a5fad60 --- /dev/null +++ b/frontend/src/app/lightning/nodes-channels/node-channels.component.html @@ -0,0 +1,2 @@ +
+
diff --git a/frontend/src/app/lightning/nodes-channels/node-channels.component.scss b/frontend/src/app/lightning/nodes-channels/node-channels.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/lightning/nodes-channels/node-channels.component.ts b/frontend/src/app/lightning/nodes-channels/node-channels.component.ts new file mode 100644 index 000000000..9d6d7df2b --- /dev/null +++ b/frontend/src/app/lightning/nodes-channels/node-channels.component.ts @@ -0,0 +1,138 @@ +import { formatNumber } from '@angular/common'; +import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { ECharts, EChartsOption, TreemapSeriesOption } from 'echarts'; +import { Observable, tap } from 'rxjs'; +import { lerpColor } from 'src/app/shared/graphs.utils'; +import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe'; +import { LightningApiService } from '../lightning-api.service'; +import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; +import { StateService } from '../../services/state.service'; + +@Component({ + selector: 'app-node-channels', + templateUrl: './node-channels.component.html', + styleUrls: ['./node-channels.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NodeChannels implements OnChanges { + @Input() publicKey: string; + + chartInstance: ECharts; + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + + channelsObservable$: Observable; + isLoading: true; + + constructor( + @Inject(LOCALE_ID) public locale: string, + private lightningApiService: LightningApiService, + private amountShortenerPipe: AmountShortenerPipe, + private zone: NgZone, + private router: Router, + private stateService: StateService, + ) {} + + ngOnChanges(): void { + this.prepareChartOptions(null); + + this.channelsObservable$ = this.lightningApiService.getChannelsByNodeId$(this.publicKey, -1, 'active') + .pipe( + tap((response) => { + const biggestCapacity = response.body[0].capacity; + this.prepareChartOptions(response.body.map(channel => { + return { + name: channel.node.alias, + value: channel.capacity, + shortId: channel.short_id, + id: channel.id, + itemStyle: { + color: lerpColor('#1E88E5', '#D81B60', Math.pow(channel.capacity / biggestCapacity, 0.4)), + } + }; + })); + }) + ); + } + + prepareChartOptions(data): void { + this.chartOptions = { + tooltip: { + trigger: 'item', + textStyle: { + align: 'left', + } + }, + series: [ + { + left: 0, + right: 0, + bottom: 0, + top: 0, + roam: false, + type: 'treemap', + data: data, + nodeClick: 'link', + progressive: 100, + tooltip: { + show: true, + backgroundColor: 'rgba(17, 19, 31, 1)', + borderRadius: 4, + shadowColor: 'rgba(0, 0, 0, 0.5)', + textStyle: { + color: '#b1b1b1', + }, + borderColor: '#000', + formatter: (value): string => { + if (value.data.name === undefined) { + return ``; + } + let capacity = ''; + if (value.data.value > 100000000) { + capacity = formatNumber(Math.round(value.data.value / 100000000), this.locale, '1.2-2') + ' BTC'; + } else { + capacity = this.amountShortenerPipe.transform(value.data.value, 2) + ' sats'; + } + + return ` + ${value.data.shortId}
+ Node: ${value.name}
+ Capacity: ${capacity} + `; + } + }, + itemStyle: { + borderColor: 'black', + borderWidth: 1, + }, + breadcrumb: { + show: false, + } + } + ] + }; + } + + onChartInit(ec: ECharts): void { + if (this.chartInstance !== undefined) { + return; + } + + this.chartInstance = ec; + + this.chartInstance.on('click', (e) => { + //@ts-ignore + if (!e.data.id) { + return; + } + this.zone.run(() => { + //@ts-ignore + const url = new RelativeUrlPipe(this.stateService).transform(`/lightning/channel/${e.data.id}`); + this.router.navigate([url]); + }); + }); + } +} \ No newline at end of file diff --git a/frontend/src/app/shared/graphs.utils.ts b/frontend/src/app/shared/graphs.utils.ts index 90977e6f4..37f2d3250 100644 --- a/frontend/src/app/shared/graphs.utils.ts +++ b/frontend/src/app/shared/graphs.utils.ts @@ -91,3 +91,25 @@ export function detectWebGL() { return (gl && gl instanceof WebGLRenderingContext); } +/** + * https://gist.githubusercontent.com/rosszurowski/67f04465c424a9bc0dae/raw/90ee06c5aa84ab352eb5b233d0a8263c3d8708e5/lerp-color.js + * A linear interpolator for hexadecimal colors + * @param {String} a + * @param {String} b + * @param {Number} amount + * @example + * // returns #7F7F7F + * lerpColor('#000000', '#ffffff', 0.5) + * @returns {String} + */ +export function lerpColor(a: string, b: string, amount: number): string { + const ah = parseInt(a.replace(/#/g, ''), 16), + ar = ah >> 16, ag = ah >> 8 & 0xff, ab = ah & 0xff, + bh = parseInt(b.replace(/#/g, ''), 16), + br = bh >> 16, bg = bh >> 8 & 0xff, bb = bh & 0xff, + rr = ar + amount * (br - ar), + rg = ag + amount * (bg - ag), + rb = ab + amount * (bb - ab); + + return '#' + ((1 << 24) + (rr << 16) + (rg << 8) + rb | 0).toString(16).slice(1); +} \ No newline at end of file