mirror of
https://github.com/mempool/mempool.git
synced 2026-08-20 13:28:00 +02:00
Merge pull request #5881 from mempool/mononaut/sighash-highlighting
experimental sighash highlighting
This commit is contained in:
commit
093dffe3ab
11 changed files with 760 additions and 25 deletions
|
|
@ -147,7 +147,7 @@
|
|||
</div>
|
||||
|
||||
|
||||
<app-transactions-list #txList [transactions]="[transaction]" [transactionPage]="true" [txPreview]="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>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@
|
|||
<ng-template ngFor let-vin let-vindex="index" [ngForOf]="tx.vin.slice(0, getVinLimit(tx))" [ngForTrackBy]="trackByIndexFn">
|
||||
<tr [ngClass]="{
|
||||
'assetBox': (assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded) || inputIndex === vindex,
|
||||
'highlight': this.addresses.length && ((vin.prevout?.scriptpubkey_type !== 'p2pk' && addresses.includes(vin.prevout?.scriptpubkey_address)) || this.addresses.includes(vin.prevout?.scriptpubkey.slice(2, -2)))
|
||||
'highlight': this.addresses.length && ((vin.prevout?.scriptpubkey_type !== 'p2pk' && addresses.includes(vin.prevout?.scriptpubkey_address)) || this.addresses.includes(vin.prevout?.scriptpubkey.slice(2, -2))),
|
||||
'sigged': selectedSig && selectedSig.txIndex === i && sigHighlights.vin[vindex],
|
||||
}">
|
||||
<td class="arrow-td">
|
||||
<ng-template [ngIf]="vin.prevout === null && !vin.is_pegin" [ngIfElse]="hasPrevout">
|
||||
|
|
@ -54,6 +55,36 @@
|
|||
</ng-template>
|
||||
</ng-template>
|
||||
</td>
|
||||
@if (tx['_showSignatures']) {
|
||||
<td class="sig-td">
|
||||
<div class="sig-stack">
|
||||
@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 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()"
|
||||
[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>
|
||||
</span>
|
||||
}
|
||||
}
|
||||
@if (tx['_sigs'][vindex].length > 7) {
|
||||
<span class="sig sig-overflow">
|
||||
+{{ tx['_sigs'][vindex].length - 7 }}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
}
|
||||
<td class="address-cell">
|
||||
<div [ngSwitch]="true">
|
||||
<ng-container *ngSwitchCase="vin.is_coinbase"><span i18n="transactions-list.coinbase">Coinbase</span><ng-template [ngIf]="network !== 'liquid' && network !== 'liquidtestnet'"> <span i18n="transactions-list.newly-generated-coins">(Newly Generated Coins)</span></ng-template><br /><a placement="bottom" [ngbTooltip]="vin.scriptsig | hex2ascii"><span class="badge badge-secondary scriptmessage longer">{{ vin.scriptsig | hex2ascii }}</span></a></ng-container>
|
||||
|
|
@ -106,21 +137,32 @@
|
|||
</tr>
|
||||
<tr *ngIf="showOrdData[tx.txid + '-vin-' + vindex]?.show" [ngClass]="{
|
||||
'assetBox': (assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded) || inputIndex === vindex,
|
||||
'highlight': addresses?.length && (addresses.includes(vin.prevout?.scriptpubkey_address) || (vin.prevout?.scriptpubkey_type === 'p2pk' && addresses.includes(vin.prevout?.scriptpubkey.slice(2, -2))))
|
||||
'highlight': addresses?.length && (addresses.includes(vin.prevout?.scriptpubkey_address) || (vin.prevout?.scriptpubkey_type === 'p2pk' && addresses.includes(vin.prevout?.scriptpubkey.slice(2, -2)))),
|
||||
'sigged': selectedSig && selectedSig.txIndex === i && sigHighlights.vin[vindex],
|
||||
}">
|
||||
<td></td>
|
||||
@if (tx['_showSignatures']) {
|
||||
<td></td>
|
||||
}
|
||||
<td colspan="2">
|
||||
<app-ord-data [inscriptions]="showOrdData[tx.txid + '-vin-' + vindex]['inscriptions']" [type]="'vin'"></app-ord-data>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="(showDetails$ | async) === true">
|
||||
<td colspan="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">
|
||||
<tr>
|
||||
<td i18n="transactions-list.scriptsig.asm|ScriptSig (ASM)">ScriptSig (ASM)</td>
|
||||
<td style="text-align: left;" [innerHTML]="vin.scriptsig_asm | asmStyler"></td>
|
||||
<td style="text-align: left;">
|
||||
<app-asm
|
||||
[asm]="vin.scriptsig_asm"
|
||||
[annotations]="{ signatures: tx['_sigmap'], selectedSig: selectedSig?.txIndex === i && selectedSig?.vindex === vindex ? selectedSig.sig : null }"
|
||||
(showSigInfo)="showSigInfo(i, vindex, $event)"
|
||||
(hideSigInfo)="hideSigInfo()"
|
||||
></app-asm>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transactions-list.scriptsig.hex|ScriptSig (HEX)">ScriptSig (HEX)</td>
|
||||
|
|
@ -130,27 +172,47 @@
|
|||
<tr *ngIf="vin.witness" class="vin-witness">
|
||||
<td i18n="transactions-list.witness">Witness</td>
|
||||
<td style="text-align: left;">
|
||||
<ng-container *ngFor="let witness of vin.witness; index as i">
|
||||
<ng-container *ngFor="let witness of vin.witness; index as windex">
|
||||
<p class="witness-item">
|
||||
<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()"
|
||||
[ngbTooltip]="sighashLabels[sigInfo.sig.sighash]">
|
||||
<fa-icon [icon]="['fas', 'key']" [fixedWidth]="true"></fa-icon>
|
||||
</span>
|
||||
</ng-container>
|
||||
@if (witness.length > 1000) {
|
||||
@if (!showFullWitness[vindex][i]) {
|
||||
@if (!showFullWitness[vindex][windex]) {
|
||||
{{ witness | slice:0:1000 }}
|
||||
} @else {
|
||||
{{ witness }}
|
||||
}
|
||||
} @else if (witness) {
|
||||
{{ witness }}
|
||||
<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()"
|
||||
[ngbTooltip]="sighashLabels[sigInfo.sig.sighash]">{{witness.slice(-2)}}</span>
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-template #plainSig>
|
||||
{{ witness }}
|
||||
</ng-template>
|
||||
} @else {
|
||||
<empty>
|
||||
}
|
||||
</p>
|
||||
@if (witness.length > 1000) {
|
||||
<div style="display: flex;">
|
||||
@if (!showFullWitness[vindex][i]) {
|
||||
@if (!showFullWitness[vindex][windex]) {
|
||||
<span>...</span>
|
||||
}
|
||||
<label class="btn btn-sm btn-primary mt-2" (click)="toggleShowFullWitness(vindex, i)" style="margin-left: auto;">
|
||||
@if (!showFullWitness[vindex][i]) {
|
||||
<label class="btn btn-sm btn-primary mt-2" (click)="toggleShowFullWitness(vindex, windex)" style="margin-left: auto;">
|
||||
@if (!showFullWitness[vindex][windex]) {
|
||||
<span i18n="show-all">Show all</span>
|
||||
} @else {
|
||||
<span i18n="show-less">Show less</span>
|
||||
|
|
@ -201,7 +263,7 @@
|
|||
</tr>
|
||||
</ng-template>
|
||||
<tr *ngIf="tx.vin.length > getVinLimit(tx)">
|
||||
<td colspan="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>
|
||||
|
|
@ -221,7 +283,8 @@
|
|||
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx.vout.slice(0, getVoutLimit(tx))" [ngForTrackBy]="trackByIndexFn">
|
||||
<tr [ngClass]="{
|
||||
'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
|
||||
'highlight': this.addresses.length && ((vout.scriptpubkey_type !== 'p2pk' && addresses.includes(vout.scriptpubkey_address)) || this.addresses.includes(vout.scriptpubkey.slice(2, -2)))
|
||||
'highlight': this.addresses.length && ((vout.scriptpubkey_type !== 'p2pk' && addresses.includes(vout.scriptpubkey_address)) || this.addresses.includes(vout.scriptpubkey.slice(2, -2))),
|
||||
'sigged': selectedSig && selectedSig.txIndex === i && sigHighlights.vout[vindex],
|
||||
}">
|
||||
<td class="address-cell">
|
||||
<app-address-text
|
||||
|
|
@ -303,14 +366,15 @@
|
|||
|
||||
<tr *ngIf="showOrdData[tx.txid + '-vout-' + vindex]?.show" [ngClass]="{
|
||||
'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
|
||||
'highlight': addresses?.length && (addresses.includes(vout.scriptpubkey_address) || (vout.scriptpubkey_type === 'p2pk' && addresses.includes(vout.scriptpubkey.slice(2, -2))))
|
||||
'highlight': addresses?.length && (addresses.includes(vout.scriptpubkey_address) || (vout.scriptpubkey_type === 'p2pk' && addresses.includes(vout.scriptpubkey.slice(2, -2)))),
|
||||
'sigged': selectedSig && selectedSig.txIndex === i && sigHighlights.vout[vindex],
|
||||
}">
|
||||
<td colspan="3">
|
||||
<app-ord-data [runestone]="showOrdData[tx.txid + '-vout-' + vindex]['runestone']" [runeInfo]="showOrdData[tx.txid + '-vout-' + vindex]['runeInfo']" [type]="'vout'"></app-ord-data>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="(showDetails$ | async) === true">
|
||||
<td colspan="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>
|
||||
|
|
@ -335,7 +399,7 @@
|
|||
</tr>
|
||||
</ng-template>
|
||||
<tr *ngIf="tx.vout.length > getVoutLimit(tx)">
|
||||
<td colspan="3" class="text-center">
|
||||
<td [attr.colspan]="3" class="text-center">
|
||||
<button class="btn btn-sm btn-primary mt-2" (click)="showMoreOutputs(tx)">
|
||||
<span *ngIf="getVoutLimit(tx, true) >= tx.vout.length; else showMoreOutputsLabel" i18n="show-all">Show all</span>
|
||||
<ng-template #showMoreOutputsLabel>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,17 @@
|
|||
.col {
|
||||
&:first-child {
|
||||
padding-right: 0;
|
||||
td:last-child {
|
||||
padding-right: calc(0.3em + 15px);
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
padding-left: 0;
|
||||
td:first-child {
|
||||
padding-left: calc(0.3em + 15px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arrow-td {
|
||||
width: 30px;
|
||||
|
|
@ -24,6 +38,118 @@
|
|||
color: var(--grey);
|
||||
}
|
||||
|
||||
.sig-td {
|
||||
width: 30px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sig-stack {
|
||||
position: relative;
|
||||
width: 30px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.sig-td:hover {
|
||||
.sig-stack {
|
||||
.sig-key {
|
||||
@for $i from 1 through 8 {
|
||||
&:nth-child(#{$i}) {
|
||||
transform: translate(#{($i - 1) * 6}px, #{($i - 1) * 6}px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sig-overflow {
|
||||
opacity: 1;
|
||||
transform: translate(30px, 40px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sig {
|
||||
cursor: pointer;
|
||||
|
||||
&.sig-overflow {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.sighash-0 {
|
||||
--sigColor: var(--green);
|
||||
}
|
||||
&.sighash-1 {
|
||||
--sigColor: var(--green);
|
||||
}
|
||||
&.sighash-2 {
|
||||
--sigColor: gold;
|
||||
}
|
||||
&.sighash-3 {
|
||||
--sigColor: cornflowerblue;
|
||||
}
|
||||
&.sighash-129 {
|
||||
--sigColor: darkviolet;
|
||||
}
|
||||
&.sighash-130 {
|
||||
--sigColor: darkorange;
|
||||
}
|
||||
&.sighash-131 {
|
||||
--sigColor: var(--pink);
|
||||
}
|
||||
&.sig-no-lock {
|
||||
--sigColor: var(--red);
|
||||
}
|
||||
color: var(--sigColor);
|
||||
|
||||
&.sig-key {
|
||||
@for $i from 1 through 8 {
|
||||
&:nth-child(#{$i}) {
|
||||
transform: translate(#{($i - 1) * 3}px, #{($i - 1) * 3}px);
|
||||
color: color-mix(in srgb, var(--sigColor) #{100 - ($i - 1) * 10} + '%', black);
|
||||
z-index: #{10 - $i};
|
||||
}
|
||||
}
|
||||
@for $i from 4 through 8 {
|
||||
&:nth-child(#{$i}) {
|
||||
transform: translate(9px, 9px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, &.hovered {
|
||||
color: color-mix(in srgb, var(--sigColor) 80%, white);
|
||||
}
|
||||
|
||||
::ng-deep svg path {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.sig-key {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transition: transform 0.2s ease;
|
||||
pointer-events: none;
|
||||
|
||||
&.sig-inline {
|
||||
position: relative;
|
||||
pointer-events: auto;
|
||||
margin-right: -5px;
|
||||
}
|
||||
}
|
||||
|
||||
.sig-overflow {
|
||||
// background-color: var(--tertiary);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
padding: 5px;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
display: block;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.mobile-bottomcol {
|
||||
margin-top: 15px;
|
||||
@media (min-width: 992px) {
|
||||
|
|
@ -142,6 +268,10 @@ h2 {
|
|||
background-color: var(--stat-box-bg);
|
||||
}
|
||||
|
||||
.sigged {
|
||||
background-color: #ffffff22;
|
||||
}
|
||||
|
||||
.summary {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -15,6 +15,9 @@ import { OrdApiService } from '@app/services/ord-api.service';
|
|||
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',
|
||||
|
|
@ -22,7 +25,7 @@ import { ADDRESS_SIMILARITY_THRESHOLD, AddressMatch, AddressSimilarity, AddressT
|
|||
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;
|
||||
|
|
@ -39,12 +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() 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();
|
||||
|
|
@ -58,6 +65,14 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||
showOrdData: { [key: string]: { show: boolean; inscriptions?: Inscription[]; runestone?: Runestone, runeInfo?: { [id: string]: { etching: Etching; txid: string; } }; } } = {};
|
||||
similarityMatches: Map<string, Map<string, { score: number, match: AddressMatch, group: number }>> = new Map();
|
||||
|
||||
selectedSig: { txIndex: number, vindex: number, sig: SigInfo } | null = null;
|
||||
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,
|
||||
|
|
@ -68,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) => {
|
||||
|
|
@ -200,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 => {
|
||||
|
|
@ -278,6 +311,20 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// process signature data
|
||||
tx['_sigs'] = tx.vin.map(vin => processInputSignatures(vin));
|
||||
tx['_sigmap'] = tx['_sigs'].reduce((map, sigs, vindex) => {
|
||||
sigs.forEach(sig => {
|
||||
map[sig.signature] = { sig, vindex };
|
||||
});
|
||||
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));
|
||||
|
|
@ -500,8 +547,64 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||
}
|
||||
}
|
||||
|
||||
showSigInfo(txIndex: number, vindex: number, sig: SigInfo): void {
|
||||
this.selectedSig = { txIndex, vindex, sig };
|
||||
this.sigHighlights = { vin: [], vout: [] };
|
||||
for (let i = 0; i < this.transactions[txIndex].vin.length; i++) {
|
||||
this.sigHighlights.vin.push(
|
||||
i === vindex ||
|
||||
!(Sighash.isACP(sig.sighash))
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < this.transactions[txIndex].vout.length; i++) {
|
||||
this.sigHighlights.vout.push(
|
||||
!(Sighash.isNone(sig.sighash)) && (
|
||||
!(Sighash.isSingle(sig.sighash)) ||
|
||||
i === vindex
|
||||
)
|
||||
);
|
||||
}
|
||||
this.ref.markForCheck();
|
||||
}
|
||||
|
||||
hideSigInfo(): void {
|
||||
this.selectedSig = null;
|
||||
this.sigHighlights = { vin: [], vout: [] };
|
||||
this.ref.markForCheck();
|
||||
}
|
||||
|
||||
updateSignaturesMode(): void {
|
||||
this.signaturesMode = this.signaturesOverride || this.forceSignaturesMode || this.signaturesPreference || 'interesting';
|
||||
if (this.transactions?.length) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
30
frontend/src/app/shared/components/asm/asm.component.html
Normal file
30
frontend/src/app/shared/components/asm/asm.component.html
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<div class="asm">
|
||||
@for (instruction of instructions; track instruction.instruction) {
|
||||
<span [class]='opcodeStyles.get(instruction.instruction)'>OP_{{instruction.instruction}}</span>
|
||||
@for (arg of instruction.args; track arg) {
|
||||
<ng-container *ngIf="annotations.signatures[arg] as sigInfo; else plainArg">
|
||||
<span class="sig sig-key sig-inline sighash-{{sigInfo.sig.sighash}}"
|
||||
[class.hovered]="annotations.selectedSig && annotations.selectedSig === sigInfo.sig"
|
||||
(mouseenter)="doShowSigInfo(sigInfo.sig)"
|
||||
(mouseleave)="doHideSigInfo()"
|
||||
[title]="sighashLabels[sigInfo.sig.sighash]">
|
||||
<fa-icon [icon]="['fas', 'key']" [fixedWidth]="true"></fa-icon>
|
||||
</span>
|
||||
<ng-container *ngIf="sigInfo.sig.sighash !== 0; else plainSig">
|
||||
{{arg.slice(0, -2)}}<span class="sig sighash-{{sigInfo.sig.sighash}}"
|
||||
[class.hovered]="annotations.selectedSig && annotations.selectedSig === sigInfo.sig"
|
||||
(mouseenter)="doShowSigInfo(sigInfo.sig)"
|
||||
(mouseleave)="doHideSigInfo()"
|
||||
[title]="sighashLabels[sigInfo.sig.sighash]">{{arg.slice(-2)}}</span>
|
||||
</ng-container>
|
||||
<ng-template #plainSig>
|
||||
{{ arg }}
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<ng-template #plainArg>
|
||||
{{ arg }}
|
||||
</ng-template>
|
||||
}
|
||||
<br>
|
||||
}
|
||||
</div>
|
||||
53
frontend/src/app/shared/components/asm/asm.component.scss
Normal file
53
frontend/src/app/shared/components/asm/asm.component.scss
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
.sig {
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
&.sighash-0 {
|
||||
--sigColor: var(--green);
|
||||
}
|
||||
&.sighash-1 {
|
||||
--sigColor: var(--green);
|
||||
}
|
||||
&.sighash-2 {
|
||||
--sigColor: gold;
|
||||
}
|
||||
&.sighash-3 {
|
||||
--sigColor: cornflowerblue;
|
||||
}
|
||||
&.sighash-129 {
|
||||
--sigColor: darkviolet;
|
||||
}
|
||||
&.sighash-130 {
|
||||
--sigColor: darkorange;
|
||||
}
|
||||
&.sighash-131 {
|
||||
--sigColor: var(--pink);
|
||||
}
|
||||
&.sig-no-lock {
|
||||
--sigColor: var(--red);
|
||||
}
|
||||
color: var(--sigColor);
|
||||
|
||||
&:hover, &.hovered {
|
||||
color: color-mix(in srgb, var(--sigColor) 60%, white);
|
||||
}
|
||||
|
||||
::ng-deep svg path {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.sig-key {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transition: transform 0.2s ease;
|
||||
pointer-events: none;
|
||||
|
||||
&.sig-inline {
|
||||
position: relative;
|
||||
pointer-events: auto;
|
||||
margin-left: 4px;
|
||||
margin-right: -4px;
|
||||
}
|
||||
}
|
||||
178
frontend/src/app/shared/components/asm/asm.component.ts
Normal file
178
frontend/src/app/shared/components/asm/asm.component.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, SimpleChanges } from '@angular/core';
|
||||
import { SigInfo, SighashLabels } from '@app/shared/transaction.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-asm',
|
||||
templateUrl: './asm.component.html',
|
||||
styleUrls: ['./asm.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class AsmComponent {
|
||||
@Input() asm: string;
|
||||
@Input() crop: number = 0;
|
||||
@Input() annotations: {
|
||||
signatures: Record<string, { sig: SigInfo, vindex: number }>,
|
||||
selectedSig: SigInfo | null
|
||||
} = {
|
||||
signatures: {},
|
||||
selectedSig: null
|
||||
};
|
||||
@Output() showSigInfo = new EventEmitter<SigInfo>();
|
||||
@Output() hideSigInfo = new EventEmitter<void>();
|
||||
|
||||
instructions: { instruction: string, args: string[] }[] = [];
|
||||
sighashLabels: Record<number, string> = SighashLabels;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.parseASM();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['asm']) {
|
||||
this.parseASM();
|
||||
}
|
||||
}
|
||||
|
||||
parseASM(): void {
|
||||
let instructions = this.asm.split('OP_');
|
||||
// trim instructions to a whole number of instructions with at most `crop` characters total
|
||||
if (this.crop) {
|
||||
let chars = 0;
|
||||
for (let i = 0; i < instructions.length; i++) {
|
||||
chars += instructions[i].length + 3;
|
||||
if (chars > this.crop) {
|
||||
instructions = instructions.slice(0, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.instructions = instructions.filter(instruction => instruction.trim() !== '').map(instruction => {
|
||||
const parts = instruction.split(' ');
|
||||
return {
|
||||
instruction: parts[0],
|
||||
args: parts.slice(1)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
doShowSigInfo(sig: SigInfo): void {
|
||||
this.showSigInfo.emit(sig);
|
||||
}
|
||||
|
||||
doHideSigInfo(): void {
|
||||
this.hideSigInfo.emit();
|
||||
}
|
||||
|
||||
readonly opcodeStyles: Map<string, string> = new Map([
|
||||
// Constants
|
||||
['0', 'constants'],
|
||||
['FALSE', 'constants'],
|
||||
['TRUE', 'constants'],
|
||||
...Array.from({length: 75}, (_, i) => [`PUSHBYTES_${i + 1}`, 'constants']),
|
||||
['PUSHDATA1', 'constants'],
|
||||
['PUSHDATA2', 'constants'],
|
||||
['PUSHDATA4', 'constants'],
|
||||
['PUSHNUM_NEG1', 'constants'],
|
||||
...Array.from({length: 16}, (_, i) => [`PUSHNUM_${i + 1}`, 'constants']),
|
||||
|
||||
// Control flow
|
||||
['NOP', 'control'],
|
||||
['IF', 'control'],
|
||||
['NOTIF', 'control'],
|
||||
['ELSE', 'control'],
|
||||
['ENDIF', 'control'],
|
||||
['VERIFY', 'control'],
|
||||
...Array.from({length: 70}, (_, i) => [`RETURN_${i + 186}`, 'control']),
|
||||
|
||||
// Stack
|
||||
['TOALTSTACK', 'stack'],
|
||||
['FROMALTSTACK', 'stack'],
|
||||
['IFDUP', 'stack'],
|
||||
['DEPTH', 'stack'],
|
||||
['DROP', 'stack'],
|
||||
['DUP', 'stack'],
|
||||
['NIP', 'stack'],
|
||||
['OVER', 'stack'],
|
||||
['PICK', 'stack'],
|
||||
['ROLL', 'stack'],
|
||||
['ROT', 'stack'],
|
||||
['SWAP', 'stack'],
|
||||
['TUCK', 'stack'],
|
||||
['2DROP', 'stack'],
|
||||
['2DUP', 'stack'],
|
||||
['3DUP', 'stack'],
|
||||
['2OVER', 'stack'],
|
||||
['2ROT', 'stack'],
|
||||
['2SWAP', 'stack'],
|
||||
|
||||
// String
|
||||
['CAT', 'splice'],
|
||||
['SUBSTR', 'splice'],
|
||||
['LEFT', 'splice'],
|
||||
['RIGHT', 'splice'],
|
||||
['SIZE', 'splice'],
|
||||
|
||||
// Logic
|
||||
['INVERT', 'logic'],
|
||||
['AND', 'logic'],
|
||||
['OR', 'logic'],
|
||||
['XOR', 'logic'],
|
||||
['EQUAL', 'logic'],
|
||||
['EQUALVERIFY', 'logic'],
|
||||
|
||||
// Arithmetic
|
||||
['1ADD', 'arithmetic'],
|
||||
['1SUB', 'arithmetic'],
|
||||
['2MUL', 'arithmetic'],
|
||||
['2DIV', 'arithmetic'],
|
||||
['NEGATE', 'arithmetic'],
|
||||
['ABS', 'arithmetic'],
|
||||
['NOT', 'arithmetic'],
|
||||
['0NOTEQUAL', 'arithmetic'],
|
||||
['ADD', 'arithmetic'],
|
||||
['SUB', 'arithmetic'],
|
||||
['MUL', 'arithmetic'],
|
||||
['DIV', 'arithmetic'],
|
||||
['MOD', 'arithmetic'],
|
||||
['LSHIFT', 'arithmetic'],
|
||||
['RSHIFT', 'arithmetic'],
|
||||
['BOOLAND', 'arithmetic'],
|
||||
['BOOLOR', 'arithmetic'],
|
||||
['NUMEQUAL', 'arithmetic'],
|
||||
['NUMEQUALVERIFY', 'arithmetic'],
|
||||
['NUMNOTEQUAL', 'arithmetic'],
|
||||
['LESSTHAN', 'arithmetic'],
|
||||
['GREATERTHAN', 'arithmetic'],
|
||||
['LESSTHANOREQUAL', 'arithmetic'],
|
||||
['GREATERTHANOREQUAL', 'arithmetic'],
|
||||
['MIN', 'arithmetic'],
|
||||
['MAX', 'arithmetic'],
|
||||
['WITHIN', 'arithmetic'],
|
||||
|
||||
// Crypto
|
||||
['RIPEMD160', 'crypto'],
|
||||
['SHA1', 'crypto'],
|
||||
['SHA256', 'crypto'],
|
||||
['HASH160', 'crypto'],
|
||||
['HASH256', 'crypto'],
|
||||
['CODESEPARATOR', 'crypto'],
|
||||
['CHECKSIG', 'crypto'],
|
||||
['CHECKSIGVERIFY', 'crypto'],
|
||||
['CHECKMULTISIG', 'crypto'],
|
||||
['CHECKMULTISIGVERIFY', 'crypto'],
|
||||
['CHECKSIGADD', 'crypto'],
|
||||
|
||||
// Locktime
|
||||
['CLTV', 'locktime'],
|
||||
['CSV', 'locktime'],
|
||||
|
||||
// Reserved
|
||||
['RESERVED', 'reserved'],
|
||||
['VER', 'reserved'],
|
||||
['VERIF', 'reserved'],
|
||||
['VERNOTIF', 'reserved'],
|
||||
['RESERVED1', 'reserved'],
|
||||
['RESERVED2', 'reserved'],
|
||||
...Array.from({length: 10}, (_, i) => [`NOP${i + 1}`, 'reserved'])
|
||||
] as [string, string][]);
|
||||
}
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
import { Vin } from "../interfaces/electrs.interface";
|
||||
import { AddressType, detectAddressType } from "./address-utils";
|
||||
|
||||
const opcodes = {
|
||||
OP_FALSE: 0,
|
||||
OP_0: 0,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, fa
|
|||
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft,
|
||||
faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck,
|
||||
faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faTimeline,
|
||||
faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes, faCreditCard, faMicroscope, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes, faCreditCard, faMicroscope, faExclamationTriangle, faLockOpen } from '@fortawesome/free-solid-svg-icons';
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
import { MenuComponent } from '@components/menu/menu.component';
|
||||
import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component';
|
||||
|
|
@ -18,6 +18,7 @@ import { Hex2asciiPipe } from '@app/shared/pipes/hex2ascii/hex2ascii.pipe';
|
|||
import { Decimal2HexPipe } from '@app/shared/pipes/decimal2hex/decimal2hex.pipe';
|
||||
import { FeeRoundingPipe } from '@app/shared/pipes/fee-rounding/fee-rounding.pipe';
|
||||
import { AsmStylerPipe } from '@app/shared/pipes/asm-styler/asm-styler.pipe';
|
||||
import { AsmComponent } from '@app/shared/components/asm/asm.component';
|
||||
import { AbsolutePipe } from '@app/shared/pipes/absolute/absolute.pipe';
|
||||
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||
import { ScriptpubkeyTypePipe } from '@app/shared/pipes/scriptpubkey-type-pipe/scriptpubkey-type.pipe';
|
||||
|
|
@ -147,6 +148,7 @@ import { GithubLogin } from '../components/github-login.component/github-login.c
|
|||
NoSanitizePipe,
|
||||
Hex2asciiPipe,
|
||||
AsmStylerPipe,
|
||||
AsmComponent,
|
||||
AbsolutePipe,
|
||||
BytesPipe,
|
||||
VbytesPipe,
|
||||
|
|
@ -462,5 +464,6 @@ export class SharedModule {
|
|||
library.addIcons(faCreditCard);
|
||||
library.addIcons(faMicroscope);
|
||||
library.addIcons(faExclamationTriangle);
|
||||
library.addIcons(faLockOpen);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Transaction, Vin } from '@interfaces/electrs.interface';
|
|||
import { CpfpInfo, RbfInfo, TransactionStripped } from '@interfaces/node-api.interface';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { hash, Hash } from './sha256';
|
||||
import { AddressType } from './address-utils';
|
||||
|
||||
// Bitcoin Core default policy settings
|
||||
const MAX_STANDARD_TX_WEIGHT = 400_000;
|
||||
|
|
@ -85,6 +86,171 @@ export function isDERSig(w: string): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
export enum SighashFlag {
|
||||
DEFAULT = 0,
|
||||
ALL = 1,
|
||||
NONE = 2,
|
||||
SINGLE = 3,
|
||||
ANYONECANPAY = 0x80
|
||||
}
|
||||
|
||||
export type SighashValue =
|
||||
SighashFlag.DEFAULT |
|
||||
SighashFlag.ALL |
|
||||
SighashFlag.NONE |
|
||||
SighashFlag.SINGLE |
|
||||
(SighashFlag.ALL & SighashFlag.ANYONECANPAY) |
|
||||
(SighashFlag.NONE & SighashFlag.ANYONECANPAY) |
|
||||
(SighashFlag.SINGLE & SighashFlag.ANYONECANPAY) |
|
||||
(SighashFlag.ALL & SighashFlag.NONE);
|
||||
|
||||
export const SighashLabels: Record<number, string> = {
|
||||
'0': 'SIGHASH_DEFAULT',
|
||||
'1': 'SIGHASH_ALL',
|
||||
'2': 'SIGHASH_NONE',
|
||||
'3': 'SIGHASH_SINGLE',
|
||||
'129': 'SIGHASH_ALL | ACP',
|
||||
'130': 'SIGHASH_NONE | ACP',
|
||||
'131': 'SIGHASH_SINGLE | ACP',
|
||||
};
|
||||
|
||||
export interface SigInfo {
|
||||
signature: string;
|
||||
sighash: SighashValue;
|
||||
}
|
||||
|
||||
export class Sighash {
|
||||
static isACP(val: SighashValue): boolean {
|
||||
return val >= SighashFlag.ANYONECANPAY;
|
||||
}
|
||||
|
||||
static isNone(val: SighashValue): boolean {
|
||||
return (val & 0x7F) === SighashFlag.NONE;
|
||||
}
|
||||
|
||||
static isSingle(val: SighashValue): boolean {
|
||||
return (val & 0x7F) === SighashFlag.SINGLE;
|
||||
}
|
||||
|
||||
static isAll(val: SighashValue): boolean {
|
||||
return (val & 0x7F) === SighashFlag.ALL;
|
||||
}
|
||||
|
||||
static isDefault(val: SighashValue): boolean {
|
||||
return val === SighashFlag.DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeSighashFlag(sighash: number): SighashValue {
|
||||
if (sighash >= 0 && sighash <= 0x03 || sighash > 0x80 && sighash <= 0x83) {
|
||||
return sighash as SighashValue;
|
||||
}
|
||||
return SighashFlag.DEFAULT;
|
||||
}
|
||||
|
||||
export function extractDERSignaturesWitness(witness: string[]): SigInfo[] {
|
||||
if (!witness?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const signatures: SigInfo[] = [];
|
||||
|
||||
for (const w of witness) {
|
||||
if (isDERSig(w)) {
|
||||
signatures.push({
|
||||
signature: w,
|
||||
sighash: decodeSighashFlag(parseInt(w.slice(-2), 16)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return signatures;
|
||||
}
|
||||
|
||||
export function extractDERSignaturesASM(script_asm: string): SigInfo[] {
|
||||
if (!script_asm) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const signatures: SigInfo[] = [];
|
||||
const ops = script_asm.split(' ');
|
||||
|
||||
for (let i = 0; i < ops.length - 1; i++) {
|
||||
// Look for OP_PUSHBYTES_N followed by a hex string
|
||||
if (ops[i].startsWith('OP_PUSHBYTES_')) {
|
||||
const hexData = ops[i + 1];
|
||||
if (isDERSig(hexData)) {
|
||||
const sighash = decodeSighashFlag(parseInt(hexData.slice(-2), 16));
|
||||
signatures.push({
|
||||
signature: hexData,
|
||||
sighash
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return signatures;
|
||||
}
|
||||
|
||||
export function extractSchnorrSignatures(witnesses: string[]): SigInfo[] {
|
||||
if (!witnesses?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const signatures: SigInfo[] = [];
|
||||
|
||||
for (const witness of witnesses) {
|
||||
if (witness.length === 130) {
|
||||
signatures.push({
|
||||
signature: witness,
|
||||
sighash: decodeSighashFlag(parseInt(witness.slice(-2), 16)),
|
||||
});
|
||||
} else if (witness.length === 128) {
|
||||
signatures.push({
|
||||
signature: witness,
|
||||
sighash: SighashFlag.DEFAULT,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return signatures;
|
||||
}
|
||||
|
||||
export function processInputSignatures(vin: Vin): SigInfo[] {
|
||||
const addressType = vin.prevout?.scriptpubkey_type as AddressType;
|
||||
let signatures: SigInfo[] = [];
|
||||
switch(addressType) {
|
||||
case 'p2pk':
|
||||
case 'multisig':
|
||||
case 'p2pkh':
|
||||
signatures = extractDERSignaturesASM(vin.scriptsig_asm);
|
||||
break;
|
||||
case 'p2sh':
|
||||
signatures = [...extractDERSignaturesASM(vin.scriptsig_asm), ...extractDERSignaturesASM(vin.inner_redeemscript_asm), ...extractDERSignaturesWitness(vin.witness || [])];
|
||||
break;
|
||||
case 'v0_p2wpkh':
|
||||
signatures = extractDERSignaturesWitness(vin.witness || []);
|
||||
break;
|
||||
case 'v0_p2wsh':
|
||||
signatures = extractDERSignaturesWitness(vin.witness || []);
|
||||
break;
|
||||
case 'v1_p2tr': {
|
||||
const hasAnnex = vin.witness[vin.witness.length - 1].startsWith('50');
|
||||
const isKeyspend = vin.witness.length === (hasAnnex ? 2 : 1);
|
||||
if (isKeyspend) {
|
||||
signatures = extractSchnorrSignatures(vin.witness);
|
||||
} else {
|
||||
const stackItems = vin.witness.slice(0, hasAnnex ? -3 : -2);
|
||||
signatures = extractSchnorrSignatures(stackItems);
|
||||
}
|
||||
} break;
|
||||
default:
|
||||
// non-signed input types?
|
||||
break;
|
||||
}
|
||||
return signatures;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates most standardness rules
|
||||
*
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue