From 40dc476460769d0d75dc8ddcbb7ff572f89adf82 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sat, 27 Aug 2022 16:00:58 +0200 Subject: [PATCH] Fixes multiple bugs with outspends and channels fixes #412 --- backend/src/api/explorer/channels.routes.ts | 33 ++++++------ .../transactions-list.component.html | 12 ++--- .../transactions-list.component.ts | 53 +++++++++++-------- .../src/app/interfaces/electrs.interface.ts | 9 ++++ .../src/app/interfaces/node-api.interface.ts | 32 +++++++++++ .../lightning/channel/channel.component.html | 4 +- .../lightning/channel/channel.component.ts | 12 +++-- frontend/src/app/services/api.service.ts | 4 +- 8 files changed, 107 insertions(+), 52 deletions(-) diff --git a/backend/src/api/explorer/channels.routes.ts b/backend/src/api/explorer/channels.routes.ts index eda3a6168..2b7f3fa6d 100644 --- a/backend/src/api/explorer/channels.routes.ts +++ b/backend/src/api/explorer/channels.routes.ts @@ -70,7 +70,7 @@ class ChannelsRoutes { } } - private async $getChannelsByTransactionIds(req: Request, res: Response) { + private async $getChannelsByTransactionIds(req: Request, res: Response): Promise { try { if (!Array.isArray(req.query.txId)) { res.status(400).send('Not an array'); @@ -83,27 +83,26 @@ class ChannelsRoutes { } } const channels = await channelsApi.$getChannelsByTransactionId(txIds); - const inputs: any[] = []; - const outputs: any[] = []; + const result: any[] = []; for (const txid of txIds) { - const foundChannelInputs = channels.find((channel) => channel.closing_transaction_id === txid); - if (foundChannelInputs) { - inputs.push(foundChannelInputs); - } else { - inputs.push(null); + const inputs: any = {}; + const outputs: any = {}; + // Assuming that we only have one lightning close input in each transaction. This may not be true in the future + const foundChannelsFromInput = channels.find((channel) => channel.closing_transaction_id === txid); + if (foundChannelsFromInput) { + inputs[0] = foundChannelsFromInput; } - const foundChannelOutputs = channels.find((channel) => channel.transaction_id === txid); - if (foundChannelOutputs) { - outputs.push(foundChannelOutputs); - } else { - outputs.push(null); + const foundChannelsFromOutputs = channels.filter((channel) => channel.transaction_id === txid); + for (const output of foundChannelsFromOutputs) { + outputs[output.transaction_vout] = output; } + result.push({ + inputs, + outputs, + }); } - res.json({ - inputs: inputs, - outputs: outputs, - }); + res.json(result); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); } diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html index 17eb52878..6333b75c2 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -20,7 +20,7 @@
- + + @@ -172,7 +172,7 @@
- +
@@ -212,15 +212,15 @@
- + - + - + diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index 2a317e738..a5cfde02f 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -27,7 +27,6 @@ export class TransactionsListComponent implements OnInit, OnChanges { @Input() outputIndex: number; @Input() address: string = ''; @Input() rowLimit = 12; - @Input() channels: { inputs: any[], outputs: any[] }; @Output() loadMore = new EventEmitter(); @@ -36,8 +35,8 @@ export class TransactionsListComponent implements OnInit, OnChanges { refreshOutspends$: ReplaySubject = new ReplaySubject(); refreshChannels$: ReplaySubject = new ReplaySubject(); showDetails$ = new BehaviorSubject(false); - outspends: Outspend[][] = []; assetsMinimal: any; + transactionsLength: number = 0; constructor( public stateService: StateService, @@ -47,7 +46,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { private ref: ChangeDetectorRef, ) { } - ngOnInit() { + ngOnInit(): void { this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block)); this.stateService.networkChanged$.subscribe((network) => this.network = network); @@ -62,14 +61,17 @@ export class TransactionsListComponent implements OnInit, OnChanges { .pipe( switchMap((txIds) => this.apiService.getOutspendsBatched$(txIds)), tap((outspends: Outspend[][]) => { - this.outspends = this.outspends.concat(outspends); + const transactions = this.transactions.filter((tx) => !tx._outspends); + outspends.forEach((outspend, i) => { + transactions[i]._outspends = outspend; + }); }), ), this.stateService.utxoSpent$ .pipe( tap((utxoSpent) => { for (const i in utxoSpent) { - this.outspends[0][i] = { + this.transactions[0]._outspends[i] = { spent: true, txid: utxoSpent[i].txid, vin: utxoSpent[i].vin, @@ -81,21 +83,23 @@ export class TransactionsListComponent implements OnInit, OnChanges { .pipe( filter(() => this.stateService.env.LIGHTNING), switchMap((txIds) => this.apiService.getChannelByTxIds$(txIds)), - map((channels) => { - this.channels = channels; + tap((channels) => { + const transactions = this.transactions.filter((tx) => !tx._channels); + channels.forEach((channel, i) => { + transactions[i]._channels = channel; + }); }), ) , ).subscribe(() => this.ref.markForCheck()); } - ngOnChanges() { + ngOnChanges(): void { if (!this.transactions || !this.transactions.length) { return; } - if (this.paginated) { - this.outspends = []; - } + + this.transactionsLength = this.transactions.length; if (this.outputIndex) { setTimeout(() => { const assetBoxElements = document.getElementsByClassName('assetBox'); @@ -126,14 +130,19 @@ export class TransactionsListComponent implements OnInit, OnChanges { tx['addressValue'] = addressIn - addressOut; } }); - const txIds = this.transactions.map((tx) => tx.txid); - this.refreshOutspends$.next(txIds); - if (!this.channels) { - this.refreshChannels$.next(txIds); + const txIds = this.transactions.filter((tx) => !tx._outspends).map((tx) => tx.txid); + if (txIds.length) { + this.refreshOutspends$.next(txIds); + } + if (this.stateService.env.LIGHTNING) { + const txIds = this.transactions.filter((tx) => !tx._channels).map((tx) => tx.txid); + if (txIds.length) { + this.refreshChannels$.next(txIds); + } } } - onScroll() { + onScroll(): void { const scrollHeight = document.body.scrollHeight; const scrollTop = document.documentElement.scrollTop; if (scrollHeight > 0){ @@ -148,11 +157,11 @@ export class TransactionsListComponent implements OnInit, OnChanges { return tx.vout.some((v: any) => v.value === undefined); } - getTotalTxOutput(tx: Transaction) { + getTotalTxOutput(tx: Transaction): number { return tx.vout.map((v: Vout) => v.value || 0).reduce((a: number, b: number) => a + b); } - switchCurrency() { + switchCurrency(): void { if (this.network === 'liquid' || this.network === 'liquidtestnet') { return; } @@ -164,7 +173,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { return tx.txid + tx.status.confirmed; } - trackByIndexFn(index: number) { + trackByIndexFn(index: number): number { return index; } @@ -177,7 +186,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { return Math.pow(base, exponent); } - toggleDetails() { + toggleDetails(): void { if (this.showDetails$.value === true) { this.showDetails$.next(false); } else { @@ -185,7 +194,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { } } - loadMoreInputs(tx: Transaction) { + loadMoreInputs(tx: Transaction): void { tx['@vinLimit'] = false; this.electrsApiService.getTransaction$(tx.txid) @@ -196,7 +205,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { }); } - ngOnDestroy() { + ngOnDestroy(): void { this.outspendsSubscription.unsubscribe(); } } diff --git a/frontend/src/app/interfaces/electrs.interface.ts b/frontend/src/app/interfaces/electrs.interface.ts index 9c873d2eb..63dec7abd 100644 --- a/frontend/src/app/interfaces/electrs.interface.ts +++ b/frontend/src/app/interfaces/electrs.interface.ts @@ -1,3 +1,5 @@ +import { IChannel } from './node-api.interface'; + export interface Transaction { txid: string; version: number; @@ -19,6 +21,13 @@ export interface Transaction { deleteAfter?: number; _unblinded?: any; _deduced?: boolean; + _outspends?: Outspend[]; + _channels?: TransactionChannels; +} + +export interface TransactionChannels { + inputs: { [vin: number]: IChannel }; + outputs: { [vout: number]: IChannel }; } interface Ancestor { diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 66ee8179e..5c071a357 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -189,3 +189,35 @@ export interface IOldestNodes { city?: any, country?: any, } + +export interface IChannel { + id: number; + short_id: string; + capacity: number; + transaction_id: string; + transaction_vout: number; + closing_transaction_id: string; + closing_reason: string; + updated_at: string; + created: string; + status: number; + node_left: Node, + node_right: Node, +} + + +export interface INode { + alias: string; + public_key: string; + channels: number; + capacity: number; + base_fee_mtokens: number; + cltv_delta: number; + fee_rate: number; + is_disabled: boolean; + max_htlc_mtokens: number; + min_htlc_mtokens: number; + updated_at: string; + longitude: number; + latitude: number; +} diff --git a/frontend/src/app/lightning/channel/channel.component.html b/frontend/src/app/lightning/channel/channel.component.html index 65866273d..6d0439e76 100644 --- a/frontend/src/app/lightning/channel/channel.component.html +++ b/frontend/src/app/lightning/channel/channel.component.html @@ -65,13 +65,13 @@

Opening transaction

- +

Closing transaction

  
- +
diff --git a/frontend/src/app/lightning/channel/channel.component.ts b/frontend/src/app/lightning/channel/channel.component.ts index bbf9be36d..553173052 100644 --- a/frontend/src/app/lightning/channel/channel.component.ts +++ b/frontend/src/app/lightning/channel/channel.component.ts @@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { forkJoin, Observable, of, share, zip } from 'rxjs'; import { catchError, map, shareReplay, switchMap, tap } from 'rxjs/operators'; +import { IChannel } from 'src/app/interfaces/node-api.interface'; import { ApiService } from 'src/app/services/api.service'; import { ElectrsApiService } from 'src/app/services/electrs-api.service'; import { SeoService } from 'src/app/services/seo.service'; @@ -62,10 +63,15 @@ export class ChannelComponent implements OnInit { ); this.transactions$ = this.channel$.pipe( - switchMap((data) => { + switchMap((channel: IChannel) => { return zip([ - data.transaction_id ? this.electrsApiService.getTransaction$(data.transaction_id) : of(null), - data.closing_transaction_id ? this.electrsApiService.getTransaction$(data.closing_transaction_id) : of(null), + channel.transaction_id ? this.electrsApiService.getTransaction$(channel.transaction_id) : of(null), + channel.closing_transaction_id ? this.electrsApiService.getTransaction$(channel.closing_transaction_id).pipe( + map((tx) => { + tx._channels = { inputs: {0: channel}, outputs: {}}; + return tx; + }) + ) : of(null), ]); }), ); diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 5d89a168f..5f036c575 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -242,12 +242,12 @@ export class ApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/enterprise/info/` + name); } - getChannelByTxIds$(txIds: string[]): Observable<{ inputs: any[], outputs: any[] }> { + getChannelByTxIds$(txIds: string[]): Observable { let params = new HttpParams(); txIds.forEach((txId: string) => { params = params.append('txId[]', txId); }); - return this.httpClient.get<{ inputs: any[], outputs: any[] }>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels/txids/', { params }); + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels/txids/', { params }); } lightningSearch$(searchText: string): Observable {