mirror of
https://github.com/mempool/mempool.git
synced 2025-10-09 19:42:52 +02:00
Merge pull request #5953 from mempool/natsoni/tx-preview-fill-signatures
Fill signatures data on transaction preview page
This commit is contained in:
@@ -220,7 +220,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="data-title clip text-center coinbase" i18n="latest-blocks.coinbasetag">
|
<th class="data-title clip text-center coinbase" i18n="latest-blocks.coinbasetag">
|
||||||
<a class="title-link" [routerLink]="['/tx/preview' | relativeUrl]" [fragment]="'offline=true&hex=' + job.coinbase">
|
<a class="title-link" [routerLink]="['/tx/preview' | relativeUrl]" [fragment]="'offline=true&tx=' + job.coinbase">
|
||||||
Coinbase tag <span> </span>
|
Coinbase tag <span> </span>
|
||||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
|
@@ -45,7 +45,7 @@
|
|||||||
<app-amount [satoshis]="row.job.reward"></app-amount>
|
<app-amount [satoshis]="row.job.reward"></app-amount>
|
||||||
</td>
|
</td>
|
||||||
<td class="height">
|
<td class="height">
|
||||||
<a [routerLink]="['/tx/preview' | relativeUrl]" [fragment]="'offline=true&hex=' + row.job.coinbase">
|
<a [routerLink]="['/tx/preview' | relativeUrl]" [fragment]="'offline=true&tx=' + row.job.coinbase">
|
||||||
{{ row.job.height }}
|
{{ row.job.height }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
@@ -54,7 +54,10 @@
|
|||||||
Redirecting to transaction page...
|
Redirecting to transaction page...
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</span>
|
</span>
|
||||||
<button *ngIf="!successBroadcast" [disabled]="isLoadingBroadcast" type="button" class="btn btn-sm btn-primary btn-broadcast" i18n="transaction.broadcast|Broadcast" (click)="postTx()">Broadcast</button>
|
<button *ngIf="!successBroadcast" [disabled]="isLoadingBroadcast || missingSignatures" i18n-ngbTooltip="transaction.missing-signature"
|
||||||
|
ngbTooltip="Transaction cannot be broadcasted because it's missing signature(s)" placement="bottom"
|
||||||
|
[disableTooltip]="!missingSignatures" type="button" class="btn btn-sm btn-primary btn-broadcast"
|
||||||
|
i18n="transaction.broadcast|Broadcast" (click)="postTx()">Broadcast</button>
|
||||||
<button *ngIf="successBroadcast" type="button" class="btn btn-sm btn-success no-cursor btn-broadcast" i18n="transaction.broadcasted|Broadcasted">Broadcasted</button>
|
<button *ngIf="successBroadcast" type="button" class="btn btn-sm btn-success no-cursor btn-broadcast" i18n="transaction.broadcasted|Broadcasted">Broadcasted</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -159,11 +162,17 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="block.size">Size</td>
|
<td i18n="block.size">Size</td>
|
||||||
<td [innerHTML]="'‎' + (transaction.size | bytes: 2)"></td>
|
<td>
|
||||||
|
<span [innerHTML]="'‎' + (transaction.size + sizeFromMissingSig | bytes: 2)"></span>
|
||||||
|
<fa-icon *ngIf="tooltipSize" [ngbTooltip]="tooltipSize" class="icon-symbol" [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
||||||
<td [innerHTML]="'‎' + (transaction.weight / 4 | vbytes: 2)"></td>
|
<td>
|
||||||
|
<span [innerHTML]="'‎' + ((transaction.weight + weightFromMissingSig) / 4 | vbytes: 2)"></span>
|
||||||
|
<fa-icon *ngIf="tooltipVsize" [ngbTooltip]="tooltipVsize" class="icon-symbol" [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="adjustedVsize">
|
<tr *ngIf="adjustedVsize">
|
||||||
<td><ng-container i18n="transaction.adjusted-vsize|Transaction Adjusted VSize">Adjusted vsize</ng-container>
|
<td><ng-container i18n="transaction.adjusted-vsize|Transaction Adjusted VSize">Adjusted vsize</ng-container>
|
||||||
@@ -175,7 +184,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="block.weight">Weight</td>
|
<td i18n="block.weight">Weight</td>
|
||||||
<td [innerHTML]="'‎' + (transaction.weight | wuBytes: 2)"></td>
|
<td>
|
||||||
|
<span [innerHTML]="'‎' + (transaction.weight + weightFromMissingSig | wuBytes: 2)"></span>
|
||||||
|
<fa-icon *ngIf="tooltipWeight" [ngbTooltip]="tooltipWeight" class="icon-symbol" [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@@ -200,3 +200,8 @@
|
|||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-symbol {
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
@@ -1,8 +1,11 @@
|
|||||||
import { Component, OnInit, HostListener, ViewChild, ElementRef, OnDestroy } from '@angular/core';
|
import { Component, OnInit, HostListener, ViewChild, ElementRef, OnDestroy } from '@angular/core';
|
||||||
|
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
|
||||||
|
import { VbytesPipe } from '../../shared/pipes/bytes-pipe/vbytes.pipe';
|
||||||
|
import { WuBytesPipe } from '../../shared/pipes/bytes-pipe/wubytes.pipe';
|
||||||
import { Transaction, Vout } from '@interfaces/electrs.interface';
|
import { Transaction, Vout } from '@interfaces/electrs.interface';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { Filter, toFilters } from '../../shared/filters.utils';
|
import { Filter, toFilters } from '../../shared/filters.utils';
|
||||||
import { decodeRawTransaction, getTransactionFlags, addInnerScriptsToVin, countSigops } from '../../shared/transaction.utils';
|
import { decodeRawTransaction, getTransactionFlags, addInnerScriptsToVin, countSigops, fillUnsignedInput } from '../../shared/transaction.utils';
|
||||||
import { catchError, firstValueFrom, Subscription, switchMap, tap, throwError, timer } from 'rxjs';
|
import { catchError, firstValueFrom, Subscription, switchMap, tap, throwError, timer } from 'rxjs';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
@@ -23,6 +26,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
pushTxForm: UntypedFormGroup;
|
pushTxForm: UntypedFormGroup;
|
||||||
rawHexTransaction: string;
|
rawHexTransaction: string;
|
||||||
|
psbt: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isLoadingPrevouts: boolean;
|
isLoadingPrevouts: boolean;
|
||||||
isLoadingCpfpInfo: boolean;
|
isLoadingCpfpInfo: boolean;
|
||||||
@@ -39,6 +43,12 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
|
|||||||
isCoinbase: boolean;
|
isCoinbase: boolean;
|
||||||
broadcastSubscription: Subscription;
|
broadcastSubscription: Subscription;
|
||||||
fragmentSubscription: Subscription;
|
fragmentSubscription: Subscription;
|
||||||
|
weightFromMissingSig: number = 0;
|
||||||
|
sizeFromMissingSig: number = 0;
|
||||||
|
missingSignatures: boolean;
|
||||||
|
tooltipSize: string;
|
||||||
|
tooltipVsize: string;
|
||||||
|
tooltipWeight: string;
|
||||||
|
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
@ViewChild('graphContainer')
|
@ViewChild('graphContainer')
|
||||||
@@ -70,6 +80,9 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
|
|||||||
public seoService: SeoService,
|
public seoService: SeoService,
|
||||||
public apiService: ApiService,
|
public apiService: ApiService,
|
||||||
public relativeUrlPipe: RelativeUrlPipe,
|
public relativeUrlPipe: RelativeUrlPipe,
|
||||||
|
public bytesPipe: BytesPipe,
|
||||||
|
public vbytesPipe: VbytesPipe,
|
||||||
|
public wuBytesPipe: WuBytesPipe,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -83,15 +96,15 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
|
|||||||
this.fragmentSubscription = this.route.fragment.subscribe((fragment) => {
|
this.fragmentSubscription = this.route.fragment.subscribe((fragment) => {
|
||||||
if (fragment) {
|
if (fragment) {
|
||||||
const params = new URLSearchParams(fragment);
|
const params = new URLSearchParams(fragment);
|
||||||
const hex = params.get('hex');
|
const txData = params.get('tx');
|
||||||
if (hex) {
|
if (txData) {
|
||||||
this.pushTxForm.get('txRaw').setValue(hex);
|
this.pushTxForm.get('txRaw').setValue(txData);
|
||||||
}
|
}
|
||||||
const offline = params.get('offline');
|
const offline = params.get('offline');
|
||||||
if (offline) {
|
if (offline) {
|
||||||
this.offlineMode = offline === 'true';
|
this.offlineMode = offline === 'true';
|
||||||
}
|
}
|
||||||
if (hex && this.pushTxForm.get('txRaw').value) {
|
if (txData && this.pushTxForm.get('txRaw').value && !this.transaction) {
|
||||||
this.decodeTransaction();
|
this.decodeTransaction();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,10 +115,11 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
|
|||||||
this.resetState();
|
this.resetState();
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
try {
|
try {
|
||||||
const { tx, hex } = decodeRawTransaction(this.pushTxForm.get('txRaw').value.trim(), this.stateService.network);
|
const { tx, hex, psbt } = decodeRawTransaction(this.pushTxForm.get('txRaw').value.trim(), this.stateService.network);
|
||||||
await this.fetchPrevouts(tx);
|
await this.fetchPrevouts(tx);
|
||||||
|
this.checkSignatures(tx, hex);
|
||||||
await this.fetchCpfpInfo(tx);
|
await this.fetchCpfpInfo(tx);
|
||||||
this.processTransaction(tx, hex);
|
this.processTransaction(tx, hex, psbt);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error = error.message;
|
this.error = error.message;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -156,6 +170,45 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
|
|||||||
this.isLoadingPrevouts = false;
|
this.isLoadingPrevouts = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkSignatures(transaction: Transaction, hex: string): void {
|
||||||
|
let missingWitnessBytes = 0;
|
||||||
|
let missingNonWitnessBytes = 0;
|
||||||
|
|
||||||
|
let isSegwitTransaction = hex.substring(8, 10) === '00' && hex.substring(10, 12) === '01';
|
||||||
|
let segwitFlagSet = false;
|
||||||
|
|
||||||
|
transaction.vin.forEach(vin => {
|
||||||
|
addInnerScriptsToVin(vin);
|
||||||
|
const result = fillUnsignedInput(vin);
|
||||||
|
vin['_missingSigs'] = result.missingSigs;
|
||||||
|
if (result.addToWitness) {
|
||||||
|
missingWitnessBytes += result.bytes;
|
||||||
|
} else {
|
||||||
|
missingNonWitnessBytes += result.bytes;
|
||||||
|
}
|
||||||
|
if (!isSegwitTransaction && result.addToWitness) {
|
||||||
|
segwitFlagSet = true;
|
||||||
|
isSegwitTransaction = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (segwitFlagSet) { // we just added witness to a legacy transaction: need to add weight corresponding to segwit flag and compact sizes
|
||||||
|
missingWitnessBytes += 2; // marker and flag
|
||||||
|
missingWitnessBytes += transaction.vin.length; // 1 byte compact size per input (assume witness stack count < 253)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sizeFromMissingSig = missingWitnessBytes + missingNonWitnessBytes;
|
||||||
|
this.weightFromMissingSig = missingWitnessBytes + 4 * missingNonWitnessBytes;
|
||||||
|
|
||||||
|
if (this.weightFromMissingSig) {
|
||||||
|
this.tooltipSize = `Includes ${this.bytesPipe.transform(this.sizeFromMissingSig, 2, undefined, undefined, true)} added for missing signatures`;
|
||||||
|
this.tooltipVsize = `Includes ${this.vbytesPipe.transform(this.weightFromMissingSig / 4, 2, undefined, undefined, true)} added for missing signatures`;
|
||||||
|
this.tooltipWeight = `Includes ${this.wuBytesPipe.transform(this.weightFromMissingSig, 2, undefined, undefined, true)} added for missing signatures`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.missingSignatures = transaction.vin.some(input => input['_missingSigs'] > 0);
|
||||||
|
|
||||||
if (this.hasPrevouts) {
|
if (this.hasPrevouts) {
|
||||||
transaction.fee = transaction.vin.some(input => input.is_coinbase)
|
transaction.fee = transaction.vin.some(input => input.is_coinbase)
|
||||||
@@ -163,11 +216,16 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
|
|||||||
: transaction.vin.reduce((fee, input) => {
|
: transaction.vin.reduce((fee, input) => {
|
||||||
return fee + (input.prevout?.value || 0);
|
return fee + (input.prevout?.value || 0);
|
||||||
}, 0) - transaction.vout.reduce((sum, output) => sum + output.value, 0);
|
}, 0) - transaction.vout.reduce((sum, output) => sum + output.value, 0);
|
||||||
transaction.feePerVsize = transaction.fee / (transaction.weight / 4);
|
transaction.feePerVsize = transaction.fee / ((transaction.weight + this.weightFromMissingSig) / 4);
|
||||||
}
|
|
||||||
|
|
||||||
transaction.vin.forEach(addInnerScriptsToVin);
|
|
||||||
transaction.sigops = countSigops(transaction);
|
transaction.sigops = countSigops(transaction);
|
||||||
|
this.adjustedVsize = Math.max((transaction.weight + this.weightFromMissingSig) / 4, transaction.sigops * 5);
|
||||||
|
const adjustedFeePerVsize = transaction.fee / this.adjustedVsize;
|
||||||
|
if (adjustedFeePerVsize !== transaction.feePerVsize) {
|
||||||
|
transaction.effectiveFeePerVsize = adjustedFeePerVsize;
|
||||||
|
this.cpfpInfo = { ancestors: [], effectiveFeePerVsize: adjustedFeePerVsize };
|
||||||
|
this.hasEffectiveFeeRate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchCpfpInfo(transaction: Transaction): Promise<void> {
|
async fetchCpfpInfo(transaction: Transaction): Promise<void> {
|
||||||
@@ -177,7 +235,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
|
|||||||
this.isLoadingCpfpInfo = true;
|
this.isLoadingCpfpInfo = true;
|
||||||
const cpfpInfo: CpfpInfo[] = await firstValueFrom(this.apiService.getCpfpLocalTx$([{
|
const cpfpInfo: CpfpInfo[] = await firstValueFrom(this.apiService.getCpfpLocalTx$([{
|
||||||
txid: transaction.txid,
|
txid: transaction.txid,
|
||||||
weight: transaction.weight,
|
weight: transaction.weight + this.weightFromMissingSig,
|
||||||
sigops: transaction.sigops,
|
sigops: transaction.sigops,
|
||||||
fee: transaction.fee,
|
fee: transaction.fee,
|
||||||
vin: transaction.vin,
|
vin: transaction.vin,
|
||||||
@@ -199,13 +257,14 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processTransaction(tx: Transaction, hex: string): void {
|
processTransaction(tx: Transaction, hex: string, psbt: string): void {
|
||||||
this.transaction = tx;
|
this.transaction = tx;
|
||||||
this.rawHexTransaction = hex;
|
this.rawHexTransaction = hex;
|
||||||
|
this.psbt = psbt;
|
||||||
|
|
||||||
this.isCoinbase = this.transaction.vin[0].is_coinbase;
|
this.isCoinbase = this.transaction.vin[0].is_coinbase;
|
||||||
|
|
||||||
// Update URL fragment with hex data
|
// Update URL fragment with hex or psbt data
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
fragment: this.getCurrentFragments(),
|
fragment: this.getCurrentFragments(),
|
||||||
replaceUrl: true
|
replaceUrl: true
|
||||||
@@ -213,9 +272,6 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
this.transaction.flags = getTransactionFlags(this.transaction, this.cpfpInfo, null, this.transaction.status?.block_height || (this.stateService.latestBlockHeight + 1), this.stateService.network);
|
this.transaction.flags = getTransactionFlags(this.transaction, this.cpfpInfo, null, this.transaction.status?.block_height || (this.stateService.latestBlockHeight + 1), this.stateService.network);
|
||||||
this.filters = this.transaction.flags ? toFilters(this.transaction.flags).filter(f => f.txPage) : [];
|
this.filters = this.transaction.flags ? toFilters(this.transaction.flags).filter(f => f.txPage) : [];
|
||||||
if (this.transaction.sigops >= 0) {
|
|
||||||
this.adjustedVsize = Math.max(this.transaction.weight / 4, this.transaction.sigops * 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setupGraph();
|
this.setupGraph();
|
||||||
this.setFlowEnabled();
|
this.setFlowEnabled();
|
||||||
@@ -266,6 +322,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
|
|||||||
resetState() {
|
resetState() {
|
||||||
this.transaction = null;
|
this.transaction = null;
|
||||||
this.rawHexTransaction = null;
|
this.rawHexTransaction = null;
|
||||||
|
this.psbt = null;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
this.errorPrevouts = null;
|
this.errorPrevouts = null;
|
||||||
this.errorBroadcast = null;
|
this.errorBroadcast = null;
|
||||||
@@ -283,6 +340,12 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
|
|||||||
this.filters = [];
|
this.filters = [];
|
||||||
this.hasPrevouts = false;
|
this.hasPrevouts = false;
|
||||||
this.missingPrevouts = [];
|
this.missingPrevouts = [];
|
||||||
|
this.weightFromMissingSig = 0;
|
||||||
|
this.sizeFromMissingSig = 0;
|
||||||
|
this.missingSignatures = false;
|
||||||
|
this.tooltipSize = null;
|
||||||
|
this.tooltipVsize = null;
|
||||||
|
this.tooltipWeight = null;
|
||||||
this.stateService.markBlock$.next({});
|
this.stateService.markBlock$.next({});
|
||||||
this.mempoolBlocksSubscription?.unsubscribe();
|
this.mempoolBlocksSubscription?.unsubscribe();
|
||||||
this.broadcastSubscription?.unsubscribe();
|
this.broadcastSubscription?.unsubscribe();
|
||||||
@@ -345,7 +408,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
|
|||||||
params.set('offline', 'true');
|
params.set('offline', 'true');
|
||||||
}
|
}
|
||||||
if (this.rawHexTransaction) {
|
if (this.rawHexTransaction) {
|
||||||
params.set('hex', this.rawHexTransaction);
|
params.set('tx', this.psbt || this.rawHexTransaction); // use PSBT in fragment if available
|
||||||
}
|
}
|
||||||
return params.toString();
|
return params.toString();
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { TransactionFlags } from '@app/shared/filters.utils';
|
import { TransactionFlags } from '@app/shared/filters.utils';
|
||||||
import { getVarIntLength, parseMultisigScript, isPoint } from '@app/shared/script.utils';
|
import { getVarIntLength, parseMultisigScript, isPoint, parseTapscriptMultisig, parseTapscriptUnanimousMultisig } from '@app/shared/script.utils';
|
||||||
import { Transaction, Vin } from '@interfaces/electrs.interface';
|
import { Transaction, Vin } from '@interfaces/electrs.interface';
|
||||||
import { CpfpInfo, RbfInfo, TransactionStripped } from '@interfaces/node-api.interface';
|
import { CpfpInfo, RbfInfo, TransactionStripped } from '@interfaces/node-api.interface';
|
||||||
import { StateService } from '@app/services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
@@ -287,12 +287,12 @@ export function processInputSignatures(vin: Vin): SigInfo[] {
|
|||||||
signatures = extractDERSignaturesWitness(vin.witness || []);
|
signatures = extractDERSignaturesWitness(vin.witness || []);
|
||||||
break;
|
break;
|
||||||
case 'v1_p2tr': {
|
case 'v1_p2tr': {
|
||||||
const hasAnnex = vin.witness.length > 1 &&vin.witness[vin.witness.length - 1].startsWith('50');
|
const hasAnnex = vin.witness?.length > 1 && vin.witness[vin.witness.length - 1].startsWith('50');
|
||||||
const isKeyspend = vin.witness.length === (hasAnnex ? 2 : 1);
|
const isKeyspend = vin.witness?.length === (hasAnnex ? 2 : 1);
|
||||||
if (isKeyspend) {
|
if (isKeyspend) {
|
||||||
signatures = extractSchnorrSignatures(vin.witness);
|
signatures = extractSchnorrSignatures(vin.witness);
|
||||||
} else {
|
} else {
|
||||||
const stackItems = vin.witness.slice(0, hasAnnex ? -3 : -2);
|
const stackItems = vin.witness?.slice(0, hasAnnex ? -3 : -2);
|
||||||
signatures = extractSchnorrSignatures(stackItems);
|
signatures = extractSchnorrSignatures(stackItems);
|
||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
@@ -303,6 +303,156 @@ export function processInputSignatures(vin: Vin): SigInfo[] {
|
|||||||
return signatures;
|
return signatures;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* returns the number of missing signatures, the number of bytes to add to the transaction
|
||||||
|
* and whether these should benefit from witness discounting
|
||||||
|
* - Add a DER sig in scriptsig/witness: 71 bytes signature + 1 push or witness size byte = 72 bytes
|
||||||
|
* - Add a public key in scriptsig/witness: 33 bytes pubkey + 1 push or witness size byte = 34 bytes
|
||||||
|
* - Add a Schnorr sig in witness: 64 bytes signature + 1 witness size byte = 65 bytes
|
||||||
|
*/
|
||||||
|
export function fillUnsignedInput(vin: Vin): { missingSigs: number, bytes: number, addToWitness: boolean } {
|
||||||
|
let missingSigs = 0;
|
||||||
|
let bytes = 0;
|
||||||
|
let addToWitness = false;
|
||||||
|
|
||||||
|
const addressType = vin.prevout?.scriptpubkey_type as AddressType;
|
||||||
|
let signatures: SigInfo[] = [];
|
||||||
|
let multisig: { m: number, n: number } | null = null;
|
||||||
|
switch (addressType) {
|
||||||
|
case 'p2pk':
|
||||||
|
signatures = extractDERSignaturesASM(vin.scriptsig_asm);
|
||||||
|
if (!signatures.length) {
|
||||||
|
missingSigs = 1;
|
||||||
|
bytes = 72;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'multisig':
|
||||||
|
signatures = extractDERSignaturesASM(vin.scriptsig_asm);
|
||||||
|
multisig = parseMultisigScript(vin.prevout.scriptpubkey_asm);
|
||||||
|
if (multisig && multisig.m - signatures.length > 0) {
|
||||||
|
missingSigs = multisig.m - signatures.length;
|
||||||
|
bytes = 72 * missingSigs + 1; // add empty stack item required for OP_CHECKMULTISIG
|
||||||
|
const scriptsigLength = vin.scriptsig.length / 2;
|
||||||
|
const newLength = scriptsigLength + bytes;
|
||||||
|
if (scriptsigLength < 253 && newLength >= 253) {
|
||||||
|
bytes += 2; // Increase scriptsig's compact size from 1 to 3 bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'p2pkh':
|
||||||
|
signatures = extractDERSignaturesASM(vin.scriptsig_asm);
|
||||||
|
if (!signatures.length) {
|
||||||
|
missingSigs = 1;
|
||||||
|
bytes = 106; // 72 + 34 (sig + public key)
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'p2sh':
|
||||||
|
// Check for P2SH multisig
|
||||||
|
multisig = parseMultisigScript(vin.inner_redeemscript_asm);
|
||||||
|
if (multisig) {
|
||||||
|
signatures = extractDERSignaturesASM(vin.scriptsig_asm);
|
||||||
|
if (multisig.m - signatures.length > 0) {
|
||||||
|
missingSigs = multisig.m - signatures.length;
|
||||||
|
bytes = 72 * missingSigs + 1; // empty push required for OP_CHECKMULTISIG
|
||||||
|
const scriptsigLength = vin.scriptsig.length / 2;
|
||||||
|
const newLength = scriptsigLength + bytes;
|
||||||
|
if (scriptsigLength < 253 && newLength >= 253) {
|
||||||
|
bytes += 2; // Increase scriptsig's compact size from 1 to 3 bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// P2SH-P2WSH
|
||||||
|
if (/OP_0 OP_PUSHBYTES_32 [a-fA-F0-9]{64}/.test(vin.inner_redeemscript_asm) && vin.inner_witnessscript_asm) {
|
||||||
|
// Check for P2WSH multisig
|
||||||
|
multisig = parseMultisigScript(vin.inner_witnessscript_asm);
|
||||||
|
if (multisig) {
|
||||||
|
signatures = extractDERSignaturesWitness(vin.witness || []);
|
||||||
|
if (multisig.m - signatures.length > 0) {
|
||||||
|
missingSigs = multisig.m - signatures.length;
|
||||||
|
bytes = 72 * missingSigs + 1; // empty push required for OP_CHECKMULTISIG
|
||||||
|
addToWitness = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// P2SH-P2WPKH
|
||||||
|
if (/OP_0 OP_PUSHBYTES_20 [a-fA-F0-9]{40}/.test(vin.inner_redeemscript_asm)) {
|
||||||
|
signatures = extractDERSignaturesWitness(vin.witness || []);
|
||||||
|
if (!signatures.length) {
|
||||||
|
missingSigs = 1;
|
||||||
|
bytes = 106; // 72 + 34 (sig + public key)
|
||||||
|
addToWitness = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'v0_p2wpkh':
|
||||||
|
signatures = extractDERSignaturesWitness(vin.witness || []);
|
||||||
|
if (!signatures.length) {
|
||||||
|
missingSigs = 1;
|
||||||
|
bytes = 106; // 72 + 34 (sig + public key)
|
||||||
|
addToWitness = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'v0_p2wsh':
|
||||||
|
signatures = extractDERSignaturesWitness(vin.witness || []);
|
||||||
|
multisig = parseMultisigScript(vin.inner_witnessscript_asm);
|
||||||
|
if (multisig) {
|
||||||
|
signatures = extractDERSignaturesWitness(vin.witness || []);
|
||||||
|
if (multisig.m - signatures.length > 0) {
|
||||||
|
missingSigs = multisig.m - signatures.length;
|
||||||
|
bytes = 72 * missingSigs + 1; // empty push required for OP_CHECKMULTISIG
|
||||||
|
addToWitness = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'v1_p2tr':
|
||||||
|
const hasAnnex = vin.witness?.length > 1 && vin.witness[vin.witness.length - 1].startsWith('50');
|
||||||
|
if (vin.inner_witnessscript_asm) {
|
||||||
|
const stackItems = vin.witness.slice(0, hasAnnex ? -3 : -2);
|
||||||
|
if (/^OP_PUSHBYTES_32 [a-fA-F0-9]{64} OP_CHECKSIG$/.test(vin.inner_witnessscript_asm)) {
|
||||||
|
signatures = extractSchnorrSignatures(stackItems);
|
||||||
|
if (!signatures.length) {
|
||||||
|
missingSigs = 1;
|
||||||
|
bytes = 65;
|
||||||
|
addToWitness = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
multisig = parseTapscriptMultisig(vin.inner_witnessscript_asm);
|
||||||
|
if (multisig) {
|
||||||
|
signatures = extractSchnorrSignatures(stackItems);
|
||||||
|
if (multisig.m - signatures.length > 0) {
|
||||||
|
missingSigs = multisig.m - signatures.length;
|
||||||
|
bytes = 65 * missingSigs + (multisig.n - multisig.m); // empty witness items for each non-signing keys
|
||||||
|
addToWitness = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let unanimousMultisig = parseTapscriptUnanimousMultisig(vin.inner_witnessscript_asm);
|
||||||
|
if (unanimousMultisig) {
|
||||||
|
signatures = extractSchnorrSignatures(stackItems);
|
||||||
|
if (unanimousMultisig - signatures.length > 0) {
|
||||||
|
missingSigs = unanimousMultisig - signatures.length;
|
||||||
|
bytes = 65 * missingSigs;
|
||||||
|
addToWitness = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // Assume keyspend
|
||||||
|
signatures = extractSchnorrSignatures(vin.witness?.slice(0, hasAnnex ? -1 : undefined) || []);
|
||||||
|
if (!signatures.length) {
|
||||||
|
missingSigs = 1;
|
||||||
|
bytes = 65;
|
||||||
|
addToWitness = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return { missingSigs, bytes, addToWitness };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates most standardness rules
|
* Validates most standardness rules
|
||||||
*
|
*
|
||||||
@@ -958,7 +1108,7 @@ export function addInnerScriptsToVin(vin: Vin): void {
|
|||||||
if (vin.prevout.scriptpubkey_type === 'p2sh') {
|
if (vin.prevout.scriptpubkey_type === 'p2sh') {
|
||||||
const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0];
|
const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0];
|
||||||
vin.inner_redeemscript_asm = convertScriptSigAsm(redeemScript);
|
vin.inner_redeemscript_asm = convertScriptSigAsm(redeemScript);
|
||||||
if (vin.witness && vin.witness.length) {
|
if (vin.witness && vin.witness.length > 2) {
|
||||||
const witnessScript = vin.witness[vin.witness.length - 1];
|
const witnessScript = vin.witness[vin.witness.length - 1];
|
||||||
vin.inner_witnessscript_asm = convertScriptSigAsm(witnessScript);
|
vin.inner_witnessscript_asm = convertScriptSigAsm(witnessScript);
|
||||||
}
|
}
|
||||||
@@ -1072,7 +1222,10 @@ function fromBuffer(buffer: Uint8Array, network: string, inputs?: { key: Uint8Ar
|
|||||||
finalScriptWitness: null,
|
finalScriptWitness: null,
|
||||||
redeemScript: null,
|
redeemScript: null,
|
||||||
witnessScript: null,
|
witnessScript: null,
|
||||||
partialSigs: []
|
partialSigs: [],
|
||||||
|
tapLeafScripts: [],
|
||||||
|
tapScriptSigs: [],
|
||||||
|
tapInternalKey: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const record of inputRecords) {
|
for (const record of inputRecords) {
|
||||||
@@ -1098,6 +1251,14 @@ function fromBuffer(buffer: Uint8Array, network: string, inputs?: { key: Uint8Ar
|
|||||||
case 0x02:
|
case 0x02:
|
||||||
groups.partialSigs.push(record);
|
groups.partialSigs.push(record);
|
||||||
break;
|
break;
|
||||||
|
case 0x14:
|
||||||
|
groups.tapScriptSigs.push(record);
|
||||||
|
break;
|
||||||
|
case 0x15:
|
||||||
|
groups.tapLeafScripts.push(record);
|
||||||
|
break;
|
||||||
|
case 0x17:
|
||||||
|
groups.tapInternalKey = record;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1154,30 +1315,35 @@ function fromBuffer(buffer: Uint8Array, network: string, inputs?: { key: Uint8Ar
|
|||||||
}
|
}
|
||||||
vin.scriptsig = (vin.scriptsig || '') + uint8ArrayToHexString(pushOpcode) + uint8ArrayToHexString(redeemScript);
|
vin.scriptsig = (vin.scriptsig || '') + uint8ArrayToHexString(pushOpcode) + uint8ArrayToHexString(redeemScript);
|
||||||
vin.scriptsig_asm = convertScriptSigAsm(vin.scriptsig);
|
vin.scriptsig_asm = convertScriptSigAsm(vin.scriptsig);
|
||||||
|
vin.inner_redeemscript_asm = vin.scriptsig_asm.split(' ').reverse()[0];
|
||||||
}
|
}
|
||||||
if (groups.witnessScript && !finalizedWitness) {
|
if (groups.witnessScript && !finalizedWitness) {
|
||||||
vin.witness = (vin.witness || []).concat(uint8ArrayToHexString(groups.witnessScript.value));
|
vin.witness = (vin.witness || []).concat(uint8ArrayToHexString(groups.witnessScript.value));
|
||||||
|
vin.inner_witnessscript_asm = convertScriptSigAsm(vin.witness[vin.witness.length - 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Fill partial signatures
|
// Fill partial signatures
|
||||||
for (const record of groups.partialSigs) {
|
for (const record of groups.partialSigs) {
|
||||||
|
const signature = record.value;
|
||||||
const scriptpubkey_type = vin.prevout?.scriptpubkey_type;
|
const scriptpubkey_type = vin.prevout?.scriptpubkey_type;
|
||||||
if (scriptpubkey_type === 'v0_p2wsh' && !finalizedWitness) {
|
if (scriptpubkey_type === 'multisig' && !finalizedScriptSig) {
|
||||||
vin.witness = vin.witness || [];
|
if (signature.length > 74) {
|
||||||
vin.witness.unshift(uint8ArrayToHexString(record.value));
|
throw new Error("Signature must be <= 74 bytes");
|
||||||
|
}
|
||||||
|
const pushOpcode = new Uint8Array([signature.length]);
|
||||||
|
vin.scriptsig = uint8ArrayToHexString(pushOpcode) + uint8ArrayToHexString(signature) + (vin.scriptsig || '');
|
||||||
|
vin.scriptsig_asm = convertScriptSigAsm(vin.scriptsig);
|
||||||
}
|
}
|
||||||
if (scriptpubkey_type === 'p2sh') {
|
if (scriptpubkey_type === 'p2sh') {
|
||||||
const redeemScriptStr = vin.scriptsig_asm ? vin.scriptsig_asm.split(' ').reverse()[0] : '';
|
const redeemScriptStr = vin.scriptsig_asm ? vin.scriptsig_asm.split(' ').reverse()[0] : '';
|
||||||
if (redeemScriptStr.startsWith('00') && redeemScriptStr.length === 68 && vin.witness?.length) {
|
if (redeemScriptStr.startsWith('00') && redeemScriptStr.length === 68 && vin.witness?.length) {
|
||||||
if (!finalizedWitness) {
|
if (!finalizedWitness) {
|
||||||
vin.witness.unshift(uint8ArrayToHexString(record.value));
|
vin.witness.unshift(uint8ArrayToHexString(signature));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!finalizedScriptSig) {
|
if (!finalizedScriptSig) {
|
||||||
const signature = record.value;
|
if (signature.length > 74) {
|
||||||
if (signature.length > 73) {
|
throw new Error("Signature must be <= 74 bytes");
|
||||||
throw new Error("Signature must be <= 73 bytes");
|
|
||||||
}
|
}
|
||||||
const pushOpcode = new Uint8Array([signature.length]);
|
const pushOpcode = new Uint8Array([signature.length]);
|
||||||
vin.scriptsig = uint8ArrayToHexString(pushOpcode) + uint8ArrayToHexString(signature) + (vin.scriptsig || '');
|
vin.scriptsig = uint8ArrayToHexString(pushOpcode) + uint8ArrayToHexString(signature) + (vin.scriptsig || '');
|
||||||
@@ -1185,6 +1351,65 @@ function fromBuffer(buffer: Uint8Array, network: string, inputs?: { key: Uint8Ar
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (scriptpubkey_type === 'v0_p2wsh' && !finalizedWitness) {
|
||||||
|
vin.witness = vin.witness || [];
|
||||||
|
vin.witness.unshift(uint8ArrayToHexString(signature));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groups.tapLeafScripts.length && groups.tapInternalKey && !finalizedWitness) {
|
||||||
|
// If no signature is present, assume key spend *except* if internal key is provably unspendable
|
||||||
|
if (!groups.tapScriptSigs.length) {
|
||||||
|
if (uint8ArrayToHexString(groups.tapInternalKey.value) === '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0') {
|
||||||
|
// unspendable internal key, use the first tap leaf script provided
|
||||||
|
const record = groups.tapLeafScripts[0];
|
||||||
|
const controlBlock = uint8ArrayToHexString(record.key.slice(1));
|
||||||
|
const tapLeaf = uint8ArrayToHexString(record.value.slice(0, -1));
|
||||||
|
vin.witness = vin.witness || [];
|
||||||
|
vin.witness.unshift(tapLeaf, controlBlock);
|
||||||
|
vin.inner_witnessscript_asm = convertScriptSigAsm(tapLeaf);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// get the hash with the most signatures
|
||||||
|
const leafScriptSignatures: { [leafHash: string]: number } = {};
|
||||||
|
let maxSignatures = 0;
|
||||||
|
let scriptMostSigs = '';
|
||||||
|
|
||||||
|
for (const record of groups.tapScriptSigs) {
|
||||||
|
const leafHash = uint8ArrayToHexString(record.key.slice(33));
|
||||||
|
if (!leafScriptSignatures[leafHash]) {
|
||||||
|
leafScriptSignatures[leafHash] = 0;
|
||||||
|
}
|
||||||
|
leafScriptSignatures[leafHash]++;
|
||||||
|
if (leafScriptSignatures[leafHash] > maxSignatures) {
|
||||||
|
maxSignatures = leafScriptSignatures[leafHash];
|
||||||
|
scriptMostSigs = leafHash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the script with most signatures
|
||||||
|
for (const record of groups.tapLeafScripts) {
|
||||||
|
const leafVersion = uint8ArrayToHexString(record.value.slice(-1));
|
||||||
|
const script = uint8ArrayToHexString(record.value.slice(0, -1));
|
||||||
|
const scriptSize = uint8ArrayToHexString(compactSize(record.value.length - 1));
|
||||||
|
if (taggedHash('TapLeaf', leafVersion + scriptSize + script) === scriptMostSigs) {
|
||||||
|
// add the script
|
||||||
|
const controlBlock = uint8ArrayToHexString(record.key.slice(1));
|
||||||
|
const tapLeaf = uint8ArrayToHexString(record.value.slice(0, -1));
|
||||||
|
vin.witness = vin.witness || [];
|
||||||
|
vin.witness.unshift(tapLeaf, controlBlock);
|
||||||
|
vin.inner_witnessscript_asm = convertScriptSigAsm(tapLeaf);
|
||||||
|
// add the signatures that are part of this script
|
||||||
|
for (const sigRecord of groups.tapScriptSigs) {
|
||||||
|
const sigLeafHash = uint8ArrayToHexString(sigRecord.key.slice(33));
|
||||||
|
if (sigLeafHash === scriptMostSigs) {
|
||||||
|
vin.witness.unshift(uint8ArrayToHexString(sigRecord.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1309,7 +1534,7 @@ function decodePsbt(psbtBuffer: Uint8Array): { rawTx: Uint8Array; inputs: { key:
|
|||||||
return { rawTx, inputs };
|
return { rawTx, inputs };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodeRawTransaction(input: string, network: string): { tx: Transaction, hex: string } {
|
export function decodeRawTransaction(input: string, network: string): { tx: Transaction, hex: string, psbt?: string } {
|
||||||
if (!input.length) {
|
if (!input.length) {
|
||||||
throw new Error('Empty input');
|
throw new Error('Empty input');
|
||||||
}
|
}
|
||||||
@@ -1325,7 +1550,7 @@ export function decodeRawTransaction(input: string, network: string): { tx: Tran
|
|||||||
|
|
||||||
if (buffer[0] === 0x70 && buffer[1] === 0x73 && buffer[2] === 0x62 && buffer[3] === 0x74) { // PSBT magic bytes
|
if (buffer[0] === 0x70 && buffer[1] === 0x73 && buffer[2] === 0x62 && buffer[3] === 0x74) { // PSBT magic bytes
|
||||||
const { rawTx, inputs } = decodePsbt(buffer);
|
const { rawTx, inputs } = decodePsbt(buffer);
|
||||||
return fromBuffer(rawTx, network, inputs);
|
return { ...fromBuffer(rawTx, network, inputs), psbt: uint8ArrayToHexString(buffer) };
|
||||||
}
|
}
|
||||||
|
|
||||||
return fromBuffer(buffer, network);
|
return fromBuffer(buffer, network);
|
||||||
|
Reference in New Issue
Block a user