Fixes multiple bugs with outspends and channels

fixes #412
This commit is contained in:
softsimon 2022-08-27 16:00:58 +02:00
parent 1a756c5fa9
commit 40dc476460
No known key found for this signature in database
GPG Key ID: 488D7DCFB5A430D7
8 changed files with 107 additions and 52 deletions

View File

@ -70,7 +70,7 @@ class ChannelsRoutes {
} }
} }
private async $getChannelsByTransactionIds(req: Request, res: Response) { private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
try { try {
if (!Array.isArray(req.query.txId)) { if (!Array.isArray(req.query.txId)) {
res.status(400).send('Not an array'); res.status(400).send('Not an array');
@ -83,27 +83,26 @@ class ChannelsRoutes {
} }
} }
const channels = await channelsApi.$getChannelsByTransactionId(txIds); const channels = await channelsApi.$getChannelsByTransactionId(txIds);
const inputs: any[] = []; const result: any[] = [];
const outputs: any[] = [];
for (const txid of txIds) { for (const txid of txIds) {
const foundChannelInputs = channels.find((channel) => channel.closing_transaction_id === txid); const inputs: any = {};
if (foundChannelInputs) { const outputs: any = {};
inputs.push(foundChannelInputs); // Assuming that we only have one lightning close input in each transaction. This may not be true in the future
} else { const foundChannelsFromInput = channels.find((channel) => channel.closing_transaction_id === txid);
inputs.push(null); if (foundChannelsFromInput) {
inputs[0] = foundChannelsFromInput;
} }
const foundChannelOutputs = channels.find((channel) => channel.transaction_id === txid); const foundChannelsFromOutputs = channels.filter((channel) => channel.transaction_id === txid);
if (foundChannelOutputs) { for (const output of foundChannelsFromOutputs) {
outputs.push(foundChannelOutputs); outputs[output.transaction_vout] = output;
} else {
outputs.push(null);
} }
result.push({
inputs,
outputs,
});
} }
res.json({ res.json(result);
inputs: inputs,
outputs: outputs,
});
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);
} }

View File

@ -20,7 +20,7 @@
<div class="col"> <div class="col">
<table class="table table-borderless smaller-text table-sm table-tx-vin"> <table class="table table-borderless smaller-text table-sm table-tx-vin">
<tbody> <tbody>
<ng-template ngFor let-vin [ngForOf]="tx['@vinLimit'] ? ((tx.vin.length > rowLimit) ? tx.vin.slice(0, rowLimit - 2) : tx.vin.slice(0, rowLimit)) : tx.vin" [ngForTrackBy]="trackByIndexFn"> <ng-template ngFor let-vin let-vindex="index" [ngForOf]="tx['@vinLimit'] ? ((tx.vin.length > rowLimit) ? tx.vin.slice(0, rowLimit - 2) : tx.vin.slice(0, rowLimit)) : tx.vin" [ngForTrackBy]="trackByIndexFn">
<tr [ngClass]="{ <tr [ngClass]="{
'assetBox': assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded, 'assetBox': assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded,
'highlight': vin.prevout?.scriptpubkey_address === this.address && this.address !== '' 'highlight': vin.prevout?.scriptpubkey_address === this.address && this.address !== ''
@ -77,7 +77,7 @@
{{ vin.prevout.scriptpubkey_type?.toUpperCase() }} {{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
</ng-template> </ng-template>
<div> <div>
<app-address-labels [vin]="vin" [channel]="channels && channels.inputs[i] || null"></app-address-labels> <app-address-labels [vin]="vin" [channel]="tx._channels && tx._channels.inputs[vin.vout] || null"></app-address-labels>
</div> </div>
</ng-template> </ng-template>
</ng-container> </ng-container>
@ -172,7 +172,7 @@
</span> </span>
</a> </a>
<div> <div>
<app-address-labels [vout]="vout" [channel]="channels && channels.outputs[i] && channels.outputs[i].transaction_vout === vindex ? channels.outputs[i] : null"></app-address-labels> <app-address-labels [vout]="vout" [channel]="tx._channels && tx._channels.outputs[vindex] ? tx._channels.outputs[vindex] : null"></app-address-labels>
</div> </div>
<ng-template #scriptpubkey_type> <ng-template #scriptpubkey_type>
<ng-template [ngIf]="vout.pegout" [ngIfElse]="defaultscriptpubkey_type"> <ng-template [ngIf]="vout.pegout" [ngIfElse]="defaultscriptpubkey_type">
@ -212,15 +212,15 @@
</ng-template> </ng-template>
</td> </td>
<td class="arrow-td"> <td class="arrow-td">
<span *ngIf="!outspends[i] || vout.scriptpubkey_type === 'op_return' || vout.scriptpubkey_type === 'fee' ; else outspend" class="grey"> <span *ngIf="!tx._outspends || vout.scriptpubkey_type === 'op_return' || vout.scriptpubkey_type === 'fee' ; else outspend" class="grey">
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon> <fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
</span> </span>
<ng-template #outspend> <ng-template #outspend>
<span *ngIf="!outspends[i][vindex] || !outspends[i][vindex].spent; else spent" class="green"> <span *ngIf="!tx._outspends[vindex] || !tx._outspends[vindex].spent; else spent" class="green">
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon> <fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
</span> </span>
<ng-template #spent> <ng-template #spent>
<a *ngIf="outspends[i][vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, outspends[i][vindex].txid]" class="red"> <a *ngIf="tx._outspends[vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, tx._outspends[vindex].txid]" class="red">
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon> <fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
</a> </a>
<ng-template #outputNoTxId> <ng-template #outputNoTxId>

View File

@ -27,7 +27,6 @@ export class TransactionsListComponent implements OnInit, OnChanges {
@Input() outputIndex: number; @Input() outputIndex: number;
@Input() address: string = ''; @Input() address: string = '';
@Input() rowLimit = 12; @Input() rowLimit = 12;
@Input() channels: { inputs: any[], outputs: any[] };
@Output() loadMore = new EventEmitter(); @Output() loadMore = new EventEmitter();
@ -36,8 +35,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
refreshOutspends$: ReplaySubject<string[]> = new ReplaySubject(); refreshOutspends$: ReplaySubject<string[]> = new ReplaySubject();
refreshChannels$: ReplaySubject<string[]> = new ReplaySubject(); refreshChannels$: ReplaySubject<string[]> = new ReplaySubject();
showDetails$ = new BehaviorSubject<boolean>(false); showDetails$ = new BehaviorSubject<boolean>(false);
outspends: Outspend[][] = [];
assetsMinimal: any; assetsMinimal: any;
transactionsLength: number = 0;
constructor( constructor(
public stateService: StateService, public stateService: StateService,
@ -47,7 +46,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
private ref: ChangeDetectorRef, private ref: ChangeDetectorRef,
) { } ) { }
ngOnInit() { ngOnInit(): void {
this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block)); this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block));
this.stateService.networkChanged$.subscribe((network) => this.network = network); this.stateService.networkChanged$.subscribe((network) => this.network = network);
@ -62,14 +61,17 @@ export class TransactionsListComponent implements OnInit, OnChanges {
.pipe( .pipe(
switchMap((txIds) => this.apiService.getOutspendsBatched$(txIds)), switchMap((txIds) => this.apiService.getOutspendsBatched$(txIds)),
tap((outspends: Outspend[][]) => { 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$ this.stateService.utxoSpent$
.pipe( .pipe(
tap((utxoSpent) => { tap((utxoSpent) => {
for (const i in utxoSpent) { for (const i in utxoSpent) {
this.outspends[0][i] = { this.transactions[0]._outspends[i] = {
spent: true, spent: true,
txid: utxoSpent[i].txid, txid: utxoSpent[i].txid,
vin: utxoSpent[i].vin, vin: utxoSpent[i].vin,
@ -81,21 +83,23 @@ export class TransactionsListComponent implements OnInit, OnChanges {
.pipe( .pipe(
filter(() => this.stateService.env.LIGHTNING), filter(() => this.stateService.env.LIGHTNING),
switchMap((txIds) => this.apiService.getChannelByTxIds$(txIds)), switchMap((txIds) => this.apiService.getChannelByTxIds$(txIds)),
map((channels) => { tap((channels) => {
this.channels = channels; const transactions = this.transactions.filter((tx) => !tx._channels);
channels.forEach((channel, i) => {
transactions[i]._channels = channel;
});
}), }),
) )
, ,
).subscribe(() => this.ref.markForCheck()); ).subscribe(() => this.ref.markForCheck());
} }
ngOnChanges() { ngOnChanges(): void {
if (!this.transactions || !this.transactions.length) { if (!this.transactions || !this.transactions.length) {
return; return;
} }
if (this.paginated) {
this.outspends = []; this.transactionsLength = this.transactions.length;
}
if (this.outputIndex) { if (this.outputIndex) {
setTimeout(() => { setTimeout(() => {
const assetBoxElements = document.getElementsByClassName('assetBox'); const assetBoxElements = document.getElementsByClassName('assetBox');
@ -126,14 +130,19 @@ export class TransactionsListComponent implements OnInit, OnChanges {
tx['addressValue'] = addressIn - addressOut; tx['addressValue'] = addressIn - addressOut;
} }
}); });
const txIds = this.transactions.map((tx) => tx.txid); const txIds = this.transactions.filter((tx) => !tx._outspends).map((tx) => tx.txid);
this.refreshOutspends$.next(txIds); if (txIds.length) {
if (!this.channels) { this.refreshOutspends$.next(txIds);
this.refreshChannels$.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 scrollHeight = document.body.scrollHeight;
const scrollTop = document.documentElement.scrollTop; const scrollTop = document.documentElement.scrollTop;
if (scrollHeight > 0){ if (scrollHeight > 0){
@ -148,11 +157,11 @@ export class TransactionsListComponent implements OnInit, OnChanges {
return tx.vout.some((v: any) => v.value === undefined); 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); 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') { if (this.network === 'liquid' || this.network === 'liquidtestnet') {
return; return;
} }
@ -164,7 +173,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
return tx.txid + tx.status.confirmed; return tx.txid + tx.status.confirmed;
} }
trackByIndexFn(index: number) { trackByIndexFn(index: number): number {
return index; return index;
} }
@ -177,7 +186,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
return Math.pow(base, exponent); return Math.pow(base, exponent);
} }
toggleDetails() { toggleDetails(): void {
if (this.showDetails$.value === true) { if (this.showDetails$.value === true) {
this.showDetails$.next(false); this.showDetails$.next(false);
} else { } else {
@ -185,7 +194,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
} }
} }
loadMoreInputs(tx: Transaction) { loadMoreInputs(tx: Transaction): void {
tx['@vinLimit'] = false; tx['@vinLimit'] = false;
this.electrsApiService.getTransaction$(tx.txid) this.electrsApiService.getTransaction$(tx.txid)
@ -196,7 +205,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
}); });
} }
ngOnDestroy() { ngOnDestroy(): void {
this.outspendsSubscription.unsubscribe(); this.outspendsSubscription.unsubscribe();
} }
} }

View File

@ -1,3 +1,5 @@
import { IChannel } from './node-api.interface';
export interface Transaction { export interface Transaction {
txid: string; txid: string;
version: number; version: number;
@ -19,6 +21,13 @@ export interface Transaction {
deleteAfter?: number; deleteAfter?: number;
_unblinded?: any; _unblinded?: any;
_deduced?: boolean; _deduced?: boolean;
_outspends?: Outspend[];
_channels?: TransactionChannels;
}
export interface TransactionChannels {
inputs: { [vin: number]: IChannel };
outputs: { [vout: number]: IChannel };
} }
interface Ancestor { interface Ancestor {

View File

@ -189,3 +189,35 @@ export interface IOldestNodes {
city?: any, city?: any,
country?: 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;
}

View File

@ -65,13 +65,13 @@
<ng-container *ngIf="transactions$ | async as transactions"> <ng-container *ngIf="transactions$ | async as transactions">
<ng-template [ngIf]="transactions[0]"> <ng-template [ngIf]="transactions[0]">
<h3>Opening transaction</h3> <h3>Opening transaction</h3>
<app-transactions-list [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5" [channels]="{ inputs: [], outputs: [channel] }"></app-transactions-list> <app-transactions-list [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
</ng-template> </ng-template>
<ng-template [ngIf]="transactions[1]"> <ng-template [ngIf]="transactions[1]">
<div class="closing-header"> <div class="closing-header">
<h3 style="margin: 0;">Closing transaction</h3>&nbsp;&nbsp;<app-closing-type [type]="channel.closing_reason"></app-closing-type> <h3 style="margin: 0;">Closing transaction</h3>&nbsp;&nbsp;<app-closing-type [type]="channel.closing_reason"></app-closing-type>
</div> </div>
<app-transactions-list [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5" [channels]="{ inputs: [channel], outputs: [] }"></app-transactions-list> <app-transactions-list [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
</ng-template> </ng-template>
</ng-container> </ng-container>

View File

@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router'; import { ActivatedRoute, ParamMap } from '@angular/router';
import { forkJoin, Observable, of, share, zip } from 'rxjs'; import { forkJoin, Observable, of, share, zip } from 'rxjs';
import { catchError, map, shareReplay, switchMap, tap } from 'rxjs/operators'; 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 { ApiService } from 'src/app/services/api.service';
import { ElectrsApiService } from 'src/app/services/electrs-api.service'; import { ElectrsApiService } from 'src/app/services/electrs-api.service';
import { SeoService } from 'src/app/services/seo.service'; import { SeoService } from 'src/app/services/seo.service';
@ -62,10 +63,15 @@ export class ChannelComponent implements OnInit {
); );
this.transactions$ = this.channel$.pipe( this.transactions$ = this.channel$.pipe(
switchMap((data) => { switchMap((channel: IChannel) => {
return zip([ return zip([
data.transaction_id ? this.electrsApiService.getTransaction$(data.transaction_id) : of(null), channel.transaction_id ? this.electrsApiService.getTransaction$(channel.transaction_id) : of(null),
data.closing_transaction_id ? this.electrsApiService.getTransaction$(data.closing_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),
]); ]);
}), }),
); );

View File

@ -242,12 +242,12 @@ export class ApiService {
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + `/api/v1/enterprise/info/` + name); return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + `/api/v1/enterprise/info/` + name);
} }
getChannelByTxIds$(txIds: string[]): Observable<{ inputs: any[], outputs: any[] }> { getChannelByTxIds$(txIds: string[]): Observable<any[]> {
let params = new HttpParams(); let params = new HttpParams();
txIds.forEach((txId: string) => { txIds.forEach((txId: string) => {
params = params.append('txId[]', txId); 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<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels/txids/', { params });
} }
lightningSearch$(searchText: string): Observable<any[]> { lightningSearch$(searchText: string): Observable<any[]> {