Support for sighash display preferences

This commit is contained in:
Mononaut
2025-05-05 23:56:07 +00:00
parent 773037b857
commit b3af3820ec
4 changed files with 89 additions and 24 deletions

View File

@@ -147,7 +147,7 @@
</div>
<app-transactions-list #txList [transactions]="[transaction]" [transactionPage]="true" [txPreview]="true" [signatures]="true"></app-transactions-list>
<app-transactions-list #txList [transactions]="[transaction]" [transactionPage]="true" [txPreview]="true" forceSignaturesMode="all"></app-transactions-list>
<div class="title text-left">
<h2 i18n="transaction.details|Transaction Details">Details</h2>

View File

@@ -55,21 +55,21 @@
</ng-template>
</ng-template>
</td>
@if (signatures) {
@if (tx['_showSignatures']) {
<td class="sig-td">
<div class="sig-stack">
@if (tx['_sigs'][vindex].length === 0) {
<span class="sig sig-key sig-no-lock" title="unsigned">
@if (tx['_sigs'][vindex].length === 0 && signaturesMode === 'all') {
<span class="sig sig-key sig-no-lock" ngbTooltip="unsigned">
<fa-icon [icon]="['fas', 'lock-open']" [fixedWidth]="true"></fa-icon>
</span>
} @else {
} @else if (showSig(tx['_sigs'][vindex])) {
@for (sig of tx['_sigs'][vindex].slice(0, 7); track sig.signature; let idx = $index) {
@if (idx < 7) {
<span
class="sig sig-key sighash-{{sig.sighash}}"
(mouseenter)="showSigInfo(i, vindex, sig)"
(mouseleave)="hideSigInfo()"
[title]="sighashLabels[sig.sighash]"
[ngbTooltip]="sighashLabels[sig.sighash]"
[class.hovered]="selectedSig && selectedSig.txIndex === i && selectedSig.vindex === vindex && selectedSig.sig === sig"
>
<fa-icon [icon]="['fas', 'key']" [fixedWidth]="true"></fa-icon>
@@ -77,7 +77,7 @@
}
}
@if (tx['_sigs'][vindex].length > 7) {
<span class="sig sig-key sig-overflow">
<span class="sig sig-overflow">
+{{ tx['_sigs'][vindex].length - 7 }}
</span>
}
@@ -141,7 +141,7 @@
'sigged': selectedSig && selectedSig.txIndex === i && sigHighlights.vin[vindex],
}">
<td></td>
@if (signatures) {
@if (tx['_showSignatures']) {
<td></td>
}
<td colspan="2">
@@ -149,7 +149,7 @@
</td>
</tr>
<tr *ngIf="(showDetails$ | async) === true">
<td [attr.colspan]="signatures ? 4 : 3" class="details-container" >
<td [attr.colspan]="tx['_showSignatures'] ? 4 : 3" class="details-container" >
<table class="table table-striped table-fixed table-borderless details-table mb-3">
<tbody>
<ng-template [ngIf]="vin.scriptsig">
@@ -174,12 +174,12 @@
<td style="text-align: left;">
<ng-container *ngFor="let witness of vin.witness; index as windex">
<p class="witness-item">
<ng-container *ngIf="signatures && tx['_sigmap'][witness] as sigInfo">
<ng-container *ngIf="tx['_sigmap'][witness] as sigInfo">
<span class="sig sig-key sig-inline sighash-{{sigInfo.sig.sighash}}"
[class.hovered]="selectedSig && selectedSig.txIndex === i && selectedSig.vindex === vindex && selectedSig.sig === sigInfo.sig"
(mouseenter)="showSigInfo(i, vindex, sigInfo.sig)"
(mouseleave)="hideSigInfo()"
[title]="sighashLabels[sigInfo.sig.sighash]">
[ngbTooltip]="sighashLabels[sigInfo.sig.sighash]">
<fa-icon [icon]="['fas', 'key']" [fixedWidth]="true"></fa-icon>
</span>
</ng-container>
@@ -190,13 +190,13 @@
{{ witness }}
}
} @else if (witness) {
<ng-container *ngIf="signatures && tx['_sigmap'][witness]?.sig.sighash !== 0 && tx['_sigmap'][witness] as sigInfo; else plainSig">
<ng-container *ngIf="tx['_sigmap'][witness]?.sig.sighash !== 0 && tx['_sigmap'][witness] as sigInfo; else plainSig">
<span class="witness">
{{witness.slice(0, -2)}}<span class="sig sighash-{{sigInfo.sig.sighash}}"
[class.hovered]="selectedSig && selectedSig.txIndex === i && selectedSig.vindex === vindex && selectedSig.sig === sigInfo.sig"
(mouseenter)="showSigInfo(i, vindex, sigInfo.sig)"
(mouseleave)="hideSigInfo()"
[title]="sighashLabels[sigInfo.sig.sighash]">{{witness.slice(-2)}}</span>
[ngbTooltip]="sighashLabels[sigInfo.sig.sighash]">{{witness.slice(-2)}}</span>
</span>
</ng-container>
<ng-template #plainSig>
@@ -263,7 +263,7 @@
</tr>
</ng-template>
<tr *ngIf="tx.vin.length > getVinLimit(tx)">
<td [attr.colspan]="signatures ? 4 : 3" class="text-center">
<td [attr.colspan]="tx['_showSignatures'] ? 4 : 3" class="text-center">
<button class="btn btn-sm btn-primary mt-2" (click)="showMoreInputs(tx)">
<span *ngIf="getVinLimit(tx, true) >= tx.vin.length; else showMoreInputsLabel" i18n="show-all">Show all</span>
<ng-template #showMoreInputsLabel>
@@ -374,7 +374,7 @@
</td>
</tr>
<tr *ngIf="(showDetails$ | async) === true">
<td [attr.colspan]="signatures ? 4 : 3" class=" details-container" >
<td [attr.colspan]="tx['_showSignatures'] ? 4 : 3" class=" details-container" >
<table class="table table-striped table-borderless details-table mb-3">
<tbody>
<tr>

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef } from '@angular/core';
import { StateService } from '@app/services/state.service';
import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { StateService, SignaturesMode } from '@app/services/state.service';
import { CacheService } from '@app/services/cache.service';
import { Observable, ReplaySubject, BehaviorSubject, merge, Subscription, of, forkJoin } from 'rxjs';
import { Outspend, Transaction, Vin, Vout } from '@interfaces/electrs.interface';
@@ -16,6 +16,8 @@ import { Inscription } from '@app/shared/ord/inscription.utils';
import { Etching, Runestone } from '@app/shared/ord/rune.utils';
import { ADDRESS_SIMILARITY_THRESHOLD, AddressMatch, AddressSimilarity, AddressType, AddressTypeInfo, checkedCompareAddressStrings, detectAddressType } from '@app/shared/address-utils';
import { processInputSignatures, Sighash, SigInfo, SighashLabels } from '@app/shared/transaction.utils';
import { ActivatedRoute } from '@angular/router';
import { SighashFlag } from '../../shared/transaction.utils';
@Component({
selector: 'app-transactions-list',
@@ -23,7 +25,7 @@ import { processInputSignatures, Sighash, SigInfo, SighashLabels } from '@app/sh
styleUrls: ['./transactions-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TransactionsListComponent implements OnInit, OnChanges {
export class TransactionsListComponent implements OnInit, OnChanges, OnDestroy {
network = '';
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
showMoreIncrement = 1000;
@@ -40,13 +42,16 @@ export class TransactionsListComponent implements OnInit, OnChanges {
@Input() rowLimit = 12;
@Input() blockTime: number = 0; // Used for price calculation if all the transactions are in the same block
@Input() txPreview = false;
@Input() signatures = true;
@Input() forceSignaturesMode: SignaturesMode = null;
@Output() loadMore = new EventEmitter();
latestBlock$: Observable<BlockExtended>;
outspendsSubscription: Subscription;
currencyChangeSubscription: Subscription;
networkSubscription: Subscription;
signaturesSubscription: Subscription;
queryParamsSubscription: Subscription;
currency: string;
refreshOutspends$: ReplaySubject<string[]> = new ReplaySubject();
refreshChannels$: ReplaySubject<string[]> = new ReplaySubject();
@@ -64,6 +69,10 @@ export class TransactionsListComponent implements OnInit, OnChanges {
sigHighlights: { vin: boolean[], vout: boolean[] } = { vin: [], vout: [] };
sighashLabels = SighashLabels;
signaturesPreference: SignaturesMode = null;
signaturesOverride: SignaturesMode = null;
signaturesMode: SignaturesMode = 'interesting';
constructor(
public stateService: StateService,
private cacheService: CacheService,
@@ -74,11 +83,29 @@ export class TransactionsListComponent implements OnInit, OnChanges {
private ref: ChangeDetectorRef,
private priceService: PriceService,
private storageService: StorageService,
) { }
private route: ActivatedRoute,
) {
this.signaturesMode = this.forceSignaturesMode || this.stateService.signaturesMode$.value;
}
ngOnInit(): void {
this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.networkSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.signaturesSubscription = this.stateService.signaturesMode$.subscribe((mode) => {
this.signaturesMode = mode;
this.updateSignaturesMode();
});
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
console.log('query params', params);
if (params['sigs'] && ['all', 'interesting', 'none'].includes(params['sigs'])) {
this.signaturesOverride = params['sigs'] as SignaturesMode;
this.updateSignaturesMode();
} else {
this.signaturesOverride = null;
this.updateSignaturesMode();
}
});
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
this.assetsService.getAssetsMinimalJson$.subscribe((assets) => {
@@ -206,12 +233,12 @@ export class TransactionsListComponent implements OnInit, OnChanges {
}
const confirmedTxs = this.transactions.filter((tx) => tx.status.confirmed).length;
this.transactions.forEach((tx) => {
tx['@voutLimit'] = true;
tx['@vinLimit'] = true;
if (tx['addressValue'] !== undefined) {
return;
}
tx['_showSignatures'] = false;
tx['_interestingSignatures'] = false;
if (this.addresses?.length) {
const addressIn = tx.vout.map(v => {
@@ -293,6 +320,11 @@ export class TransactionsListComponent implements OnInit, OnChanges {
});
return map;
}, {});
if (!tx['_interestingSignatures']) {
tx['_interestingSignatures'] = tx['_sigs'].some(sigs => sigs.some(sig => this.sigIsInteresting(sig)));
}
tx['_showSignatures'] = this.shouldShowSignatures(tx);
}
tx.largeInput = tx.largeInput || tx.vin.some(vin => (vin?.prevout?.value > 1000000000));
@@ -541,8 +573,36 @@ export class TransactionsListComponent implements OnInit, OnChanges {
this.ref.markForCheck();
}
updateSignaturesMode(): void {
this.signaturesMode = this.signaturesOverride || this.forceSignaturesMode || this.signaturesPreference || 'interesting';
for (const tx of this.transactions) {
tx['_showSignatures'] = this.shouldShowSignatures(tx);
}
}
showSig(sigs: SigInfo[]): boolean {
return this.signaturesMode === 'all' || (this.signaturesMode === 'interesting' && sigs.some(sig => this.sigIsInteresting(sig)));
}
sigIsInteresting(sig: SigInfo): boolean {
return sig.sighash !== SighashFlag.DEFAULT && sig.sighash !== SighashFlag.ALL;
}
shouldShowSignatures(tx): boolean {
switch (this.signaturesMode) {
case 'all':
return true;
case 'interesting':
return tx['_interestingSignatures'];
default:
return false;
}
}
ngOnDestroy(): void {
this.outspendsSubscription.unsubscribe();
this.currencyChangeSubscription?.unsubscribe();
this.networkSubscription.unsubscribe();
this.signaturesSubscription.unsubscribe();
}
}

View File

@@ -42,6 +42,8 @@ export interface Customization {
};
}
export type SignaturesMode = 'all' | 'interesting' | 'none' | null;
export interface Env {
MAINNET_ENABLED: boolean;
TESTNET_ENABLED: boolean;
@@ -150,6 +152,7 @@ export class StateService {
backend$ = new BehaviorSubject<'esplora' | 'electrum' | 'none'>('esplora');
networkChanged$ = new ReplaySubject<string>(1);
lightningChanged$ = new ReplaySubject<boolean>(1);
signaturesMode$: BehaviorSubject<SignaturesMode>;
blocksSubject$ = new BehaviorSubject<BlockExtended[]>([]);
blocks$: Observable<BlockExtended[]>;
transactions$ = new BehaviorSubject<TransactionStripped[]>(null);
@@ -332,6 +335,8 @@ export class StateService {
this.blocksSubject$.next([]);
});
this.signaturesMode$ = new BehaviorSubject<SignaturesMode>(this.storageService.getValue('signatures-enabled') as SignaturesMode || null);
this.blockVSize = this.env.BLOCK_WEIGHT_UNITS / 4;
this.blocks$ = this.blocksSubject$.pipe(filter(blocks => blocks != null && blocks.length > 0));