diff --git a/frontend/src/app/components/address-labels/address-labels.component.ts b/frontend/src/app/components/address-labels/address-labels.component.ts index dbac4ab11..f2018bfe5 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.ts +++ b/frontend/src/app/components/address-labels/address-labels.component.ts @@ -34,7 +34,7 @@ export class AddressLabelsComponent implements OnChanges { } handleChannel() { - this.label = `Channel open: ${this.channel.alias_left} <> ${this.channel.alias_right}`; + this.label = `Channel open: ${this.channel.node_left.alias} <> ${this.channel.node_right.alias}`; } handleVin() { diff --git a/frontend/src/app/lightning/channel/channel-box/channel-box.component.spec.ts b/frontend/src/app/lightning/channel/channel-box/channel-box.component.spec.ts new file mode 100644 index 000000000..ae9463a6c --- /dev/null +++ b/frontend/src/app/lightning/channel/channel-box/channel-box.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChannelBoxComponent } from './channel-box.component'; + +describe('ChannelBoxComponent', () => { + let component: ChannelBoxComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ChannelBoxComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ChannelBoxComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 72e451b0d..aec19de5f 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.html +++ b/frontend/src/app/lightning/channels-list/channels-list.component.html @@ -1,44 +1,39 @@ -
+
+

Channels ({{ response.totalItems }})

+ +
+
+ + +
+
+ - - - - - - - - - - + + + - - - - - - - - - - - -
Node Alias StatusFee RateCapacityChannel ID
- - - - - - - - - - - -
+ +
+ + + + Node Alias +   + Status + Fee Rate + Capacity + Channel ID + + @@ -50,7 +45,7 @@
- +
{{ node.channels }} channels
@@ -65,7 +60,37 @@ - + {{ channel.short_id }} + + +

Channels

+ + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.ts b/frontend/src/app/lightning/channels-list/channels-list.component.ts index f6b0c448b..debf2467a 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.ts +++ b/frontend/src/app/lightning/channels-list/channels-list.component.ts @@ -1,5 +1,7 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { BehaviorSubject, combineLatest, merge, Observable, of } from 'rxjs'; +import { map, startWith, switchMap } from 'rxjs/operators'; import { LightningApiService } from '../lightning-api.service'; @Component({ @@ -8,16 +10,53 @@ import { LightningApiService } from '../lightning-api.service'; styleUrls: ['./channels-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ChannelsListComponent implements OnChanges { +export class ChannelsListComponent implements OnInit, OnChanges { @Input() publicKey: string; - channels$: Observable; + channels$: Observable; + + // @ts-ignore + paginationSize: 'sm' | 'lg' = 'md'; + paginationMaxSize = 10; + itemsPerPage = 25; + page = 1; + channelsPage$ = new BehaviorSubject(1); + channelStatusForm: FormGroup; + defaultStatus = 'open'; constructor( private lightningApiService: LightningApiService, - ) { } + private formBuilder: FormBuilder, + ) { + this.channelStatusForm = this.formBuilder.group({ + status: [this.defaultStatus], + }); + } + + ngOnInit() { + if (document.body.clientWidth < 670) { + this.paginationSize = 'sm'; + this.paginationMaxSize = 3; + } + } ngOnChanges(): void { - this.channels$ = this.lightningApiService.getChannelsByNodeId$(this.publicKey); + this.channels$ = combineLatest([ + this.channelsPage$, + this.channelStatusForm.get('status').valueChanges.pipe(startWith(this.defaultStatus)) + ]) + .pipe( + switchMap(([page, status]) =>this.lightningApiService.getChannelsByNodeId$(this.publicKey, (page -1) * this.itemsPerPage, status)), + map((response) => { + return { + channels: response.body, + totalItems: parseInt(response.headers.get('x-total-count'), 10) + }; + }), + ); + } + + pageChange(page: number) { + this.channelsPage$.next(page); } } diff --git a/frontend/src/app/lightning/lightning-api.service.ts b/frontend/src/app/lightning/lightning-api.service.ts index a49923546..9197f4f02 100644 --- a/frontend/src/app/lightning/lightning-api.service.ts +++ b/frontend/src/app/lightning/lightning-api.service.ts @@ -20,12 +20,14 @@ export class LightningApiService { return this.httpClient.get(API_BASE_URL + '/channels/' + shortId); } - getChannelsByNodeId$(publicKey: string): Observable { + getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable { let params = new HttpParams() .set('public_key', publicKey) + .set('index', index) + .set('status', status) ; - return this.httpClient.get(API_BASE_URL + '/channels', { params }); + return this.httpClient.get(API_BASE_URL + '/channels', { params, observe: 'response' }); } getLatestStatistics$(): Observable { diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index 80e357c22..93bb67e61 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -88,7 +88,6 @@
-

Channels

diff --git a/lightning-backend/src/api/explorer/channels.api.ts b/lightning-backend/src/api/explorer/channels.api.ts index f7b0a5751..95d660bb4 100644 --- a/lightning-backend/src/api/explorer/channels.api.ts +++ b/lightning-backend/src/api/explorer/channels.api.ts @@ -73,10 +73,16 @@ class ChannelsApi { } } - public async $getChannelsForNode(public_key: string): Promise { + public async $getChannelsForNode(public_key: string, index: number, length: number, status: string): Promise { try { - const query = `SELECT n1.alias AS alias_left, n2.alias AS alias_right, channels.*, ns1.channels AS channels_left, ns1.capacity AS capacity_left, ns2.channels AS channels_right, ns2.capacity AS capacity_right FROM channels LEFT JOIN nodes AS n1 ON n1.public_key = channels.node1_public_key LEFT JOIN nodes AS n2 ON n2.public_key = channels.node2_public_key LEFT JOIN node_stats AS ns1 ON ns1.public_key = channels.node1_public_key LEFT JOIN node_stats AS ns2 ON ns2.public_key = channels.node2_public_key WHERE (ns1.id = (SELECT MAX(id) FROM node_stats WHERE public_key = channels.node1_public_key) AND ns2.id = (SELECT MAX(id) FROM node_stats WHERE public_key = channels.node2_public_key)) AND (node1_public_key = ? OR node2_public_key = ?) ORDER BY channels.capacity DESC`; - const [rows]: any = await DB.query(query, [public_key, public_key]); + // Default active and inactive channels + let statusQuery = '< 2'; + // Closed channels only + if (status === 'closed') { + statusQuery = '= 2'; + } + const query = `SELECT n1.alias AS alias_left, n2.alias AS alias_right, channels.*, ns1.channels AS channels_left, ns1.capacity AS capacity_left, ns2.channels AS channels_right, ns2.capacity AS capacity_right FROM channels LEFT JOIN nodes AS n1 ON n1.public_key = channels.node1_public_key LEFT JOIN nodes AS n2 ON n2.public_key = channels.node2_public_key LEFT JOIN node_stats AS ns1 ON ns1.public_key = channels.node1_public_key LEFT JOIN node_stats AS ns2 ON ns2.public_key = channels.node2_public_key WHERE (ns1.id = (SELECT MAX(id) FROM node_stats WHERE public_key = channels.node1_public_key) AND ns2.id = (SELECT MAX(id) FROM node_stats WHERE public_key = channels.node2_public_key)) AND (node1_public_key = ? OR node2_public_key = ?) AND status ${statusQuery} ORDER BY channels.capacity DESC LIMIT ?, ?`; + const [rows]: any = await DB.query(query, [public_key, public_key, index, length]); const channels = rows.map((row) => this.convertChannel(row)); return channels; } catch (e) { @@ -85,13 +91,30 @@ class ChannelsApi { } } + public async $getChannelsCountForNode(public_key: string, status: string): Promise { + try { + // Default active and inactive channels + let statusQuery = '< 2'; + // Closed channels only + if (status === 'closed') { + statusQuery = '= 2'; + } + const query = `SELECT COUNT(*) AS count FROM channels WHERE (node1_public_key = ? OR node2_public_key = ?) AND status ${statusQuery}`; + const [rows]: any = await DB.query(query, [public_key, public_key]); + return rows[0]['count']; + } catch (e) { + logger.err('$getChannelsForNode error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } + private convertChannel(channel: any): any { return { 'id': channel.id, 'short_id': channel.short_id, 'capacity': channel.capacity, 'transaction_id': channel.transaction_id, - 'transaction_vout': channel.void, + 'transaction_vout': channel.transaction_vout, 'updated_at': channel.updated_at, 'created': channel.created, 'status': channel.status, diff --git a/lightning-backend/src/api/explorer/channels.routes.ts b/lightning-backend/src/api/explorer/channels.routes.ts index 61b4846e1..fd3b3fac0 100644 --- a/lightning-backend/src/api/explorer/channels.routes.ts +++ b/lightning-backend/src/api/explorer/channels.routes.ts @@ -10,7 +10,7 @@ class ChannelsRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'channels/txids', this.$getChannelsByTransactionIds) .get(config.MEMPOOL.API_URL_PREFIX + 'channels/search/:search', this.$searchChannelsById) .get(config.MEMPOOL.API_URL_PREFIX + 'channels/:short_id', this.$getChannel) - .get(config.MEMPOOL.API_URL_PREFIX + 'channels', this.$getChannels) + .get(config.MEMPOOL.API_URL_PREFIX + 'channels', this.$getChannelsForNode) ; } @@ -36,13 +36,18 @@ class ChannelsRoutes { } } - private async $getChannels(req: Request, res: Response) { + private async $getChannelsForNode(req: Request, res: Response) { try { if (typeof req.query.public_key !== 'string') { res.status(501).send('Missing parameter: public_key'); return; } - const channels = await channelsApi.$getChannelsForNode(req.query.public_key); + const index = parseInt(typeof req.query.index === 'string' ? req.query.index : '0', 10) || 0; + const status: string = typeof req.query.status === 'string' ? req.query.status : ''; + const length = 25; + const channels = await channelsApi.$getChannelsForNode(req.query.public_key, index, length, status); + const channelsCount = await channelsApi.$getChannelsCountForNode(req.query.public_key, status); + res.header('X-Total-Count', channelsCount.toString()); res.json(channels); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e);