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> </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"> <div class="title text-left">
<h2 i18n="transaction.details|Transaction Details">Details</h2> <h2 i18n="transaction.details|Transaction Details">Details</h2>

View File

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

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef } from '@angular/core'; import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { StateService } from '@app/services/state.service'; import { StateService, SignaturesMode } from '@app/services/state.service';
import { CacheService } from '@app/services/cache.service'; import { CacheService } from '@app/services/cache.service';
import { Observable, ReplaySubject, BehaviorSubject, merge, Subscription, of, forkJoin } from 'rxjs'; import { Observable, ReplaySubject, BehaviorSubject, merge, Subscription, of, forkJoin } from 'rxjs';
import { Outspend, Transaction, Vin, Vout } from '@interfaces/electrs.interface'; 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 { Etching, Runestone } from '@app/shared/ord/rune.utils';
import { ADDRESS_SIMILARITY_THRESHOLD, AddressMatch, AddressSimilarity, AddressType, AddressTypeInfo, checkedCompareAddressStrings, detectAddressType } from '@app/shared/address-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 { processInputSignatures, Sighash, SigInfo, SighashLabels } from '@app/shared/transaction.utils';
import { ActivatedRoute } from '@angular/router';
import { SighashFlag } from '../../shared/transaction.utils';
@Component({ @Component({
selector: 'app-transactions-list', selector: 'app-transactions-list',
@@ -23,7 +25,7 @@ import { processInputSignatures, Sighash, SigInfo, SighashLabels } from '@app/sh
styleUrls: ['./transactions-list.component.scss'], styleUrls: ['./transactions-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class TransactionsListComponent implements OnInit, OnChanges { export class TransactionsListComponent implements OnInit, OnChanges, OnDestroy {
network = ''; network = '';
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId; nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
showMoreIncrement = 1000; showMoreIncrement = 1000;
@@ -40,13 +42,16 @@ export class TransactionsListComponent implements OnInit, OnChanges {
@Input() rowLimit = 12; @Input() rowLimit = 12;
@Input() blockTime: number = 0; // Used for price calculation if all the transactions are in the same block @Input() blockTime: number = 0; // Used for price calculation if all the transactions are in the same block
@Input() txPreview = false; @Input() txPreview = false;
@Input() signatures = true; @Input() forceSignaturesMode: SignaturesMode = null;
@Output() loadMore = new EventEmitter(); @Output() loadMore = new EventEmitter();
latestBlock$: Observable<BlockExtended>; latestBlock$: Observable<BlockExtended>;
outspendsSubscription: Subscription; outspendsSubscription: Subscription;
currencyChangeSubscription: Subscription; currencyChangeSubscription: Subscription;
networkSubscription: Subscription;
signaturesSubscription: Subscription;
queryParamsSubscription: Subscription;
currency: string; currency: string;
refreshOutspends$: ReplaySubject<string[]> = new ReplaySubject(); refreshOutspends$: ReplaySubject<string[]> = new ReplaySubject();
refreshChannels$: 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: [] }; sigHighlights: { vin: boolean[], vout: boolean[] } = { vin: [], vout: [] };
sighashLabels = SighashLabels; sighashLabels = SighashLabels;
signaturesPreference: SignaturesMode = null;
signaturesOverride: SignaturesMode = null;
signaturesMode: SignaturesMode = 'interesting';
constructor( constructor(
public stateService: StateService, public stateService: StateService,
private cacheService: CacheService, private cacheService: CacheService,
@@ -74,11 +83,29 @@ export class TransactionsListComponent implements OnInit, OnChanges {
private ref: ChangeDetectorRef, private ref: ChangeDetectorRef,
private priceService: PriceService, private priceService: PriceService,
private storageService: StorageService, private storageService: StorageService,
) { } private route: ActivatedRoute,
) {
this.signaturesMode = this.forceSignaturesMode || this.stateService.signaturesMode$.value;
}
ngOnInit(): void { ngOnInit(): void {
this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0])); 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') { if (this.network === 'liquid' || this.network === 'liquidtestnet') {
this.assetsService.getAssetsMinimalJson$.subscribe((assets) => { 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; const confirmedTxs = this.transactions.filter((tx) => tx.status.confirmed).length;
this.transactions.forEach((tx) => { this.transactions.forEach((tx) => {
tx['@voutLimit'] = true; tx['@voutLimit'] = true;
tx['@vinLimit'] = true; tx['@vinLimit'] = true;
if (tx['addressValue'] !== undefined) { tx['_showSignatures'] = false;
return; tx['_interestingSignatures'] = false;
}
if (this.addresses?.length) { if (this.addresses?.length) {
const addressIn = tx.vout.map(v => { const addressIn = tx.vout.map(v => {
@@ -293,6 +320,11 @@ export class TransactionsListComponent implements OnInit, OnChanges {
}); });
return map; 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)); 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(); 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 { ngOnDestroy(): void {
this.outspendsSubscription.unsubscribe(); this.outspendsSubscription.unsubscribe();
this.currencyChangeSubscription?.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 { export interface Env {
MAINNET_ENABLED: boolean; MAINNET_ENABLED: boolean;
TESTNET_ENABLED: boolean; TESTNET_ENABLED: boolean;
@@ -150,6 +152,7 @@ export class StateService {
backend$ = new BehaviorSubject<'esplora' | 'electrum' | 'none'>('esplora'); backend$ = new BehaviorSubject<'esplora' | 'electrum' | 'none'>('esplora');
networkChanged$ = new ReplaySubject<string>(1); networkChanged$ = new ReplaySubject<string>(1);
lightningChanged$ = new ReplaySubject<boolean>(1); lightningChanged$ = new ReplaySubject<boolean>(1);
signaturesMode$: BehaviorSubject<SignaturesMode>;
blocksSubject$ = new BehaviorSubject<BlockExtended[]>([]); blocksSubject$ = new BehaviorSubject<BlockExtended[]>([]);
blocks$: Observable<BlockExtended[]>; blocks$: Observable<BlockExtended[]>;
transactions$ = new BehaviorSubject<TransactionStripped[]>(null); transactions$ = new BehaviorSubject<TransactionStripped[]>(null);
@@ -332,6 +335,8 @@ export class StateService {
this.blocksSubject$.next([]); 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.blockVSize = this.env.BLOCK_WEIGHT_UNITS / 4;
this.blocks$ = this.blocksSubject$.pipe(filter(blocks => blocks != null && blocks.length > 0)); this.blocks$ = this.blocksSubject$.pipe(filter(blocks => blocks != null && blocks.length > 0));