Alternative transaction unfurl design
This commit is contained in:
@@ -13,104 +13,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a [routerLink]="['/tx/' | relativeUrl, txId]" class="tx-link">
|
||||
{{ txId }}
|
||||
</a>
|
||||
<p class="text-center mb-0">
|
||||
<a [routerLink]="['/tx/' | relativeUrl, txId]" class="tx-link">
|
||||
{{ txId }}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr *ngIf="tx.status.confirmed; else firstSeen">
|
||||
<td i18n="block.timestamp">Timestamp</td>
|
||||
<td>
|
||||
‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
</td>
|
||||
</tr>
|
||||
<ng-template #firstSeen>
|
||||
<tr>
|
||||
<td i18n="transaction.first-seen|Transaction first seen">First seen</td>
|
||||
<td *ngIf="transactionTime > 0; else notSeen">
|
||||
‎{{ transactionTime * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
</td>
|
||||
<ng-template #notSeen>
|
||||
<td>?</td>
|
||||
</ng-template>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<tr>
|
||||
<td class="td-width" i18n="dashboard.latest-transactions.amount">Amount</td>
|
||||
<td>
|
||||
<ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
|
||||
<ng-template #defaultAmount>
|
||||
<app-amount [satoshis]="getTotalTxOutput(tx)"></app-amount>
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.size">Size</td>
|
||||
<td [innerHTML]="'‎' + (tx.size | bytes: 2)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.weight">Weight</td>
|
||||
<td [innerHTML]="'‎' + (tx.weight | wuBytes: 2)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transaction.inputs">Inputs</td>
|
||||
<td *ngIf="!isCoinbase(tx); else coinbaseInputs">{{ tx.vin.length }}</td>
|
||||
<ng-template #coinbaseInputs>
|
||||
<td i18n="transactions-list.coinbase">Coinbase</td>
|
||||
</ng-template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="row graph-wrapper">
|
||||
<tx-bowtie-graph [tx]="tx" [width]="1112" [height]="346" [isLiquid]="isLiquid"></tx-bowtie-graph>
|
||||
<div class="above-bow">
|
||||
<p class="field">
|
||||
<ng-template [ngIf]="isLiquid && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
|
||||
<ng-template #defaultAmount>
|
||||
<app-amount [satoshis]="totalValue"></app-amount>
|
||||
</ng-template>
|
||||
</p>
|
||||
<p class="field" *ngIf="!isCoinbase(tx)">
|
||||
{{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="!cpfpInfo || (!cpfpInfo.bestDescendant && !cpfpInfo.ancestors.length); else cpfpFee">
|
||||
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
|
||||
<td>
|
||||
{{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
<ng-template [ngIf]="tx.status.confirmed">
|
||||
|
||||
<app-tx-fee-rating *ngIf="tx.fee && ((cpfpInfo && !cpfpInfo.bestDescendant && !cpfpInfo.ancestors.length) || !cpfpInfo)" [tx]="tx"></app-tx-fee-rating>
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-template #cpfpFee>
|
||||
<div class="overlaid">
|
||||
<ng-container [ngSwitch]="extraData">
|
||||
<table class="opreturns" *ngSwitchCase="'coinbase'">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||
<td>
|
||||
<div class="effective-fee-container">
|
||||
{{ tx.effectiveFeePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
<ng-template [ngIf]="tx.status.confirmed">
|
||||
<app-tx-fee-rating class="d-none d-lg-inline ml-2" *ngIf="tx.fee" [tx]="tx"></app-tx-fee-rating>
|
||||
</ng-template>
|
||||
</div>
|
||||
</td>
|
||||
<td class="label">Coinbase</td>
|
||||
<td class="message">{{ tx.vin[0].scriptsig | hex2ascii }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<tr>
|
||||
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
||||
<td [innerHTML]="'‎' + (tx.weight / 4 | vbytes: 2)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transaction.locktime">Locktime</td>
|
||||
<td [innerHTML]="'‎' + (tx.locktime | number)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transaction.outputs">Outputs</td>
|
||||
<td>{{ tx.vout.length }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="opreturns" *ngSwitchCase="'opreturn'">
|
||||
<tbody>
|
||||
<ng-container *ngFor="let vout of opReturns.slice(0,3)">
|
||||
<tr>
|
||||
<td class="label">OP_RETURN</td>
|
||||
<td *ngIf="vout.scriptpubkey_asm !== 'OP_RETURN'" class="message">{{ vout.scriptpubkey_asm | hex2ascii }}</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,67 @@
|
||||
}
|
||||
|
||||
.tx-link {
|
||||
display: inline-block;
|
||||
display: inline;
|
||||
font-size: 28px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.graph-wrapper {
|
||||
position: relative;
|
||||
background: #181b2d;
|
||||
padding: 10px;
|
||||
padding-bottom: 0;
|
||||
|
||||
.above-bow {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
|
||||
.field {
|
||||
font-size: 32px;
|
||||
margin: 0;
|
||||
|
||||
::ng-deep .symbol {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overlaid {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-size: 28px;
|
||||
max-width: 90%;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
|
||||
.opreturns {
|
||||
width: auto;
|
||||
margin: auto;
|
||||
table-layout: auto;
|
||||
background: #2d3348af;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
|
||||
td {
|
||||
padding: 10px 10px;
|
||||
|
||||
&.message {
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,9 @@ import {
|
||||
catchError,
|
||||
retryWhen,
|
||||
delay,
|
||||
map
|
||||
} from 'rxjs/operators';
|
||||
import { Transaction, Vout } from '../../interfaces/electrs.interface';
|
||||
import { of, merge, Subscription, Observable, Subject, timer, combineLatest, from } from 'rxjs';
|
||||
import { of, merge, Subscription, Observable, Subject, from } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { OpenGraphService } from 'src/app/services/opengraph.service';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
@@ -30,13 +29,16 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
isLoadingTx = true;
|
||||
error: any = undefined;
|
||||
errorUnblinded: any = undefined;
|
||||
transactionTime = -1;
|
||||
subscription: Subscription;
|
||||
fetchCpfpSubscription: Subscription;
|
||||
cpfpInfo: CpfpInfo | null;
|
||||
showCpfpDetails = false;
|
||||
fetchCpfp$ = new Subject<string>();
|
||||
liquidUnblinding = new LiquidUnblinding();
|
||||
isLiquid = false;
|
||||
totalValue: number;
|
||||
opReturns: Vout[];
|
||||
extraData: 'none' | 'coinbase' | 'opreturn';
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
@@ -49,7 +51,12 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit() {
|
||||
this.stateService.networkChanged$.subscribe(
|
||||
(network) => (this.network = network)
|
||||
(network) => {
|
||||
this.network = network;
|
||||
if (this.network === 'liquid' || this.network == 'liquidtestnet') {
|
||||
this.isLiquid = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.fetchCpfpSubscription = this.fetchCpfp$
|
||||
@@ -152,12 +159,9 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
|
||||
this.isLoadingTx = false;
|
||||
this.error = undefined;
|
||||
|
||||
if (!tx.status.confirmed && tx.firstSeen) {
|
||||
this.transactionTime = tx.firstSeen;
|
||||
} else {
|
||||
this.getTransactionTime();
|
||||
}
|
||||
this.totalValue = this.tx.vout.reduce((acc, v) => v.value + acc, 0);
|
||||
this.opReturns = this.getOpReturns(this.tx);
|
||||
this.extraData = this.chooseExtraData();
|
||||
|
||||
if (!this.tx.status.confirmed) {
|
||||
if (tx.cpfpChecked) {
|
||||
@@ -181,26 +185,10 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
getTransactionTime() {
|
||||
this.openGraphService.waitFor('tx-time');
|
||||
this.apiService
|
||||
.getTransactionTimes$([this.tx.txid])
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
return of(0);
|
||||
})
|
||||
)
|
||||
.subscribe((transactionTimes) => {
|
||||
this.transactionTime = transactionTimes[0];
|
||||
this.openGraphService.waitOver('tx-time');
|
||||
});
|
||||
}
|
||||
|
||||
resetTransaction() {
|
||||
this.error = undefined;
|
||||
this.tx = null;
|
||||
this.isLoadingTx = true;
|
||||
this.transactionTime = -1;
|
||||
this.cpfpInfo = null;
|
||||
this.showCpfpDetails = false;
|
||||
}
|
||||
@@ -217,6 +205,20 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
return tx.vout.map((v: Vout) => v.value || 0).reduce((a: number, b: number) => a + b);
|
||||
}
|
||||
|
||||
getOpReturns(tx: Transaction): Vout[] {
|
||||
return tx.vout.filter((v) => v.scriptpubkey_type === 'op_return' && v.scriptpubkey_asm !== 'OP_RETURN');
|
||||
}
|
||||
|
||||
chooseExtraData(): 'none' | 'opreturn' | 'coinbase' {
|
||||
if (this.isCoinbase(this.tx)) {
|
||||
return 'coinbase';
|
||||
} else if (this.opReturns?.length) {
|
||||
return 'opreturn';
|
||||
} else {
|
||||
return 'none';
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscription.unsubscribe();
|
||||
this.fetchCpfpSubscription.unsubscribe();
|
||||
|
||||
Reference in New Issue
Block a user