mirror of
https://github.com/mempool/mempool.git
synced 2024-11-20 10:21:52 +01:00
Merge pull request #2360 from mononaut/alt-tx-unfurls
Alternative transaction unfurl design
This commit is contained in:
commit
0018c865bd
@ -2,6 +2,9 @@
|
||||
|
||||
<div class="page-title">
|
||||
<h1 i18n="shared.transaction">Transaction</h1>
|
||||
<a class="tx-link" [routerLink]="['/tx/' | relativeUrl, txId]">
|
||||
<span class="truncated">{{txId.slice(0,-4)}}</span><span class="last-four">{{txId.slice(-4)}}</span>
|
||||
</a>
|
||||
<div *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" class="features">
|
||||
<app-tx-features [tx]="tx"></app-tx-features>
|
||||
<span *ngIf="cpfpInfo && cpfpInfo.bestDescendant" class="badge badge-primary mr-1">
|
||||
@ -13,104 +16,50 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a [routerLink]="['/tx/' | relativeUrl, txId]" class="tx-link">
|
||||
{{ txId }}
|
||||
</a>
|
||||
<div class="top-data row">
|
||||
<span class="field col-sm-4 text-left">
|
||||
<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>
|
||||
</span>
|
||||
<span class="field col-sm-4 text-center">‎{{ (tx.status.confirmed ? tx.status.block_time : transactionTime) * 1000 | date:'yyyy-MM-dd HH:mm' }}</span>
|
||||
<span class="field col-sm-4 text-right"><span class="label" i18n="transaction.fee|Transaction fee">Fee </span>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
|
||||
</div>
|
||||
|
||||
|
||||
<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" [network]="network"></tx-bowtie-graph>
|
||||
<div class="above-bow">
|
||||
<p class="field pair">
|
||||
<span [innerHTML]="'‎' + (tx.size | bytes: 2)"></span>
|
||||
<span [innerHTML]="'‎' + (tx.weight | wuBytes: 2)"></span>
|
||||
</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>
|
||||
|
@ -10,26 +10,10 @@
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.btn-small-height {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.arrow-green {
|
||||
color: #1a9436;
|
||||
}
|
||||
|
||||
.arrow-red {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.effective-fee-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.title {
|
||||
h2 {
|
||||
line-height: 1;
|
||||
@ -46,8 +30,9 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
align-items: baseline;
|
||||
margin-bottom: 2px;
|
||||
max-width: 100%;
|
||||
|
||||
h1 {
|
||||
font-size: 52px;
|
||||
@ -58,6 +43,43 @@
|
||||
.features {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
& > * {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tx-link {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin: 0 1em;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
|
||||
.truncated {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: -2px;
|
||||
}
|
||||
|
||||
.last-four {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.features {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.top-data {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.table {
|
||||
@ -68,8 +90,76 @@
|
||||
}
|
||||
}
|
||||
|
||||
.field {
|
||||
font-size: 32px;
|
||||
margin: 0;
|
||||
|
||||
::ng-deep .symbol {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #ffffff66;
|
||||
}
|
||||
|
||||
&.pair > *:first-child {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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';
|
||||
@ -37,6 +36,10 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
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 +52,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,6 +160,9 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
|
||||
this.isLoadingTx = false;
|
||||
this.error = undefined;
|
||||
this.totalValue = this.tx.vout.reduce((acc, v) => v.value + acc, 0);
|
||||
this.opReturns = this.getOpReturns(this.tx);
|
||||
this.extraData = this.chooseExtraData();
|
||||
|
||||
if (!tx.status.confirmed && tx.firstSeen) {
|
||||
this.transactionTime = tx.firstSeen;
|
||||
@ -217,6 +228,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();
|
||||
|
@ -0,0 +1,44 @@
|
||||
<svg *ngIf="inputs && outputs" class="bowtie" [attr.height]="(height + 10) + 'px'" [attr.width]="width + 'px'">
|
||||
<defs>
|
||||
<marker id="input-arrow" viewBox="-5 -5 10 10"
|
||||
refX="0" refY="0"
|
||||
markerUnits="strokeWidth"
|
||||
markerWidth="1.5" markerHeight="1"
|
||||
orient="auto">
|
||||
<path d="M -5 -5 L 0 0 L -5 5 L 1 5 L 1 -5 Z" stroke-width="0" [attr.fill]="gradient[0]"/>
|
||||
</marker>
|
||||
<marker id="output-arrow" viewBox="-5 -5 10 10"
|
||||
refX="0" refY="0"
|
||||
markerUnits="strokeWidth"
|
||||
markerWidth="1.5" markerHeight="1"
|
||||
orient="auto">
|
||||
<path d="M 1 -5 L 0 -5 L -5 0 L 0 5 L 1 5 Z" stroke-width="0" [attr.fill]="gradient[0]"/>
|
||||
</marker>
|
||||
<marker id="fee-arrow" viewBox="-5 -5 10 10"
|
||||
refX="0" refY="0"
|
||||
markerUnits="strokeWidth"
|
||||
markerWidth="1.5" markerHeight="1"
|
||||
orient="auto">
|
||||
</marker>
|
||||
<linearGradient id="input-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" [attr.stop-color]="gradient[0]" />
|
||||
<stop offset="100%" [attr.stop-color]="gradient[1]" />
|
||||
</linearGradient>
|
||||
<linearGradient id="output-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" [attr.stop-color]="gradient[1]" />
|
||||
<stop offset="100%" [attr.stop-color]="gradient[0]" />
|
||||
</linearGradient>
|
||||
<linearGradient id="fee-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" [attr.stop-color]="gradient[1]" />
|
||||
<stop offset="50%" [attr.stop-color]="gradient[1]" />
|
||||
<stop offset="100%" stop-color="transparent" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path [attr.d]="middle.path" class="line middle" [style]="middle.style"/>
|
||||
<ng-container *ngFor="let input of inputs">
|
||||
<path [attr.d]="input.path" class="line {{input.class}}" [style]="input.style" attr.marker-start="url(#{{input.class}}-arrow)"/>
|
||||
</ng-container>
|
||||
<ng-container *ngFor="let output of outputs">
|
||||
<path [attr.d]="output.path" class="line {{output.class}}" [style]="output.style" attr.marker-start="url(#{{output.class}}-arrow)" />
|
||||
</ng-container>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
@ -0,0 +1,15 @@
|
||||
.bowtie {
|
||||
.line {
|
||||
fill: none;
|
||||
|
||||
&.input {
|
||||
stroke: url(#input-gradient);
|
||||
}
|
||||
&.output {
|
||||
stroke: url(#output-gradient);
|
||||
}
|
||||
&.fee {
|
||||
stroke: url(#fee-gradient);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,169 @@
|
||||
import { Component, OnInit, Input, OnChanges } from '@angular/core';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
|
||||
interface SvgLine {
|
||||
path: string;
|
||||
style: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'tx-bowtie-graph',
|
||||
templateUrl: './tx-bowtie-graph.component.html',
|
||||
styleUrls: ['./tx-bowtie-graph.component.scss'],
|
||||
})
|
||||
export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
@Input() tx: Transaction;
|
||||
@Input() network: string;
|
||||
@Input() width = 1200;
|
||||
@Input() height = 600;
|
||||
@Input() combinedWeight = 100;
|
||||
@Input() minWeight = 2; //
|
||||
@Input() maxStrands = 24; // number of inputs/outputs to keep fully on-screen.
|
||||
|
||||
inputs: SvgLine[];
|
||||
outputs: SvgLine[];
|
||||
middle: SvgLine;
|
||||
isLiquid: boolean = false;
|
||||
|
||||
gradientColors = {
|
||||
'': ['#9339f4', '#105fb0'],
|
||||
bisq: ['#9339f4', '#105fb0'],
|
||||
// liquid: ['#116761', '#183550'],
|
||||
liquid: ['#09a197', '#0f62af'],
|
||||
// 'liquidtestnet': ['#494a4a', '#272e46'],
|
||||
'liquidtestnet': ['#d2d2d2', '#979797'],
|
||||
// testnet: ['#1d486f', '#183550'],
|
||||
testnet: ['#4edf77', '#10a0af'],
|
||||
// signet: ['#6f1d5d', '#471850'],
|
||||
signet: ['#d24fc8', '#a84fd2'],
|
||||
};
|
||||
|
||||
gradient: string[] = ['#105fb0', '#105fb0'];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet');
|
||||
this.gradient = this.gradientColors[this.network];
|
||||
this.initGraph();
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet');
|
||||
this.gradient = this.gradientColors[this.network];
|
||||
this.initGraph();
|
||||
}
|
||||
|
||||
initGraph(): void {
|
||||
const totalValue = this.calcTotalValue(this.tx);
|
||||
const voutWithFee = this.tx.vout.map(v => { return { type: v.scriptpubkey_type === 'fee' ? 'fee' : 'output', value: v?.value }; });
|
||||
|
||||
if (this.tx.fee && !this.isLiquid) {
|
||||
voutWithFee.unshift({ type: 'fee', value: this.tx.fee });
|
||||
}
|
||||
|
||||
this.inputs = this.initLines('in', this.tx.vin.map(v => { return {type: 'input', value: v?.prevout?.value }; }), totalValue, this.maxStrands);
|
||||
this.outputs = this.initLines('out', voutWithFee, totalValue, this.maxStrands);
|
||||
|
||||
this.middle = {
|
||||
path: `M ${(this.width / 2) - 50} ${(this.height / 2) + 0.5} L ${(this.width / 2) + 50} ${(this.height / 2) + 0.5}`,
|
||||
style: `stroke-width: ${this.combinedWeight + 0.5}; stroke: ${this.gradient[1]}`
|
||||
};
|
||||
}
|
||||
|
||||
calcTotalValue(tx: Transaction): number {
|
||||
const totalOutput = this.tx.vout.reduce((acc, v) => (v.value == null ? 0 : v.value) + acc, 0);
|
||||
// simple sum of outputs + fee for bitcoin
|
||||
if (!this.isLiquid) {
|
||||
return this.tx.fee ? totalOutput + this.tx.fee : totalOutput;
|
||||
} else {
|
||||
const totalInput = this.tx.vin.reduce((acc, v) => (v?.prevout?.value == null ? 0 : v.prevout.value) + acc, 0);
|
||||
const confidentialInputCount = this.tx.vin.reduce((acc, v) => acc + (v?.prevout?.value == null ? 1 : 0), 0);
|
||||
const confidentialOutputCount = this.tx.vout.reduce((acc, v) => acc + (v.value == null ? 1 : 0), 0);
|
||||
|
||||
// if there are unknowns on both sides, the total is indeterminate, so we'll just fudge it
|
||||
if (confidentialInputCount && confidentialOutputCount) {
|
||||
const knownInputCount = (tx.vin.length - confidentialInputCount) || 1;
|
||||
const knownOutputCount = (tx.vout.length - confidentialOutputCount) || 1;
|
||||
// assume confidential inputs/outputs have the same average value as the known ones
|
||||
const adjustedTotalInput = totalInput + ((totalInput / knownInputCount) * confidentialInputCount);
|
||||
const adjustedTotalOutput = totalOutput + ((totalOutput / knownOutputCount) * confidentialOutputCount);
|
||||
return Math.max(adjustedTotalInput, adjustedTotalOutput) || 1;
|
||||
} else {
|
||||
// otherwise knowing the actual total of one side suffices
|
||||
return Math.max(totalInput, totalOutput) || 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initLines(side: 'in' | 'out', xputs: { type: string, value: number | void }[], total: number, maxVisibleStrands: number): SvgLine[] {
|
||||
const lines = [];
|
||||
let unknownCount = 0;
|
||||
let unknownTotal = total == null ? this.combinedWeight : total;
|
||||
xputs.forEach(put => {
|
||||
if (put.value == null) {
|
||||
unknownCount++;
|
||||
} else {
|
||||
unknownTotal -= put.value as number;
|
||||
}
|
||||
});
|
||||
const unknownShare = unknownTotal / unknownCount;
|
||||
|
||||
// conceptual weights
|
||||
const weights = xputs.map((put): number => this.combinedWeight * (put.value == null ? unknownShare : put.value as number) / total);
|
||||
// actual displayed line thicknesses
|
||||
const minWeights = weights.map((w) => Math.max(this.minWeight - 1, w) + 1);
|
||||
const visibleStrands = Math.min(maxVisibleStrands, xputs.length);
|
||||
const visibleWeight = minWeights.slice(0, visibleStrands).reduce((acc, v) => v + acc, 0);
|
||||
const gaps = visibleStrands - 1;
|
||||
|
||||
const innerTop = (this.height / 2) - (this.combinedWeight / 2);
|
||||
const innerBottom = innerTop + this.combinedWeight;
|
||||
// tracks the visual bottom of the endpoints of the previous line
|
||||
let lastOuter = 0;
|
||||
let lastInner = innerTop;
|
||||
// gap between strands
|
||||
const spacing = (this.height - visibleWeight) / gaps;
|
||||
|
||||
for (let i = 0; i < xputs.length; i++) {
|
||||
const weight = weights[i];
|
||||
const minWeight = minWeights[i];
|
||||
// set the vertical position of the (center of the) outer side of the line
|
||||
let outer = lastOuter + (minWeight / 2);
|
||||
const inner = Math.min(innerBottom + (minWeight / 2), Math.max(innerTop + (minWeight / 2), lastInner + (weight / 2)));
|
||||
|
||||
// special case to center single input/outputs
|
||||
if (xputs.length === 1) {
|
||||
outer = (this.height / 2);
|
||||
}
|
||||
|
||||
lastOuter += minWeight + spacing;
|
||||
lastInner += weight;
|
||||
lines.push({
|
||||
path: this.makePath(side, outer, inner, minWeight),
|
||||
style: this.makeStyle(minWeight, xputs[i].type),
|
||||
class: xputs[i].type
|
||||
});
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
makePath(side: 'in' | 'out', outer: number, inner: number, weight: number): string {
|
||||
const start = side === 'in' ? (weight * 0.5) : this.width - (weight * 0.5);
|
||||
const center = this.width / 2 + (side === 'in' ? -45 : 45 );
|
||||
const midpoint = (start + center) / 2;
|
||||
// correct for svg horizontal gradient bug
|
||||
if (Math.round(outer) === Math.round(inner)) {
|
||||
outer -= 1;
|
||||
}
|
||||
return `M ${start} ${outer} C ${midpoint} ${outer}, ${midpoint} ${inner}, ${center} ${inner}`;
|
||||
}
|
||||
|
||||
makeStyle(minWeight, type): string {
|
||||
if (type === 'fee') {
|
||||
return `stroke-width: ${minWeight}; stroke: url(#fee-gradient)`;
|
||||
} else {
|
||||
return `stroke-width: ${minWeight}`;
|
||||
}
|
||||
}
|
||||
}
|
@ -63,6 +63,7 @@ import { StatusViewComponent } from '../components/status-view/status-view.compo
|
||||
import { FeesBoxComponent } from '../components/fees-box/fees-box.component';
|
||||
import { DifficultyComponent } from '../components/difficulty/difficulty.component';
|
||||
import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component';
|
||||
import { TxBowtieGraphComponent } from '../components/tx-bowtie-graph/tx-bowtie-graph.component';
|
||||
import { PrivacyPolicyComponent } from '../components/privacy-policy/privacy-policy.component';
|
||||
import { TrademarkPolicyComponent } from '../components/trademark-policy/trademark-policy.component';
|
||||
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
|
||||
@ -138,6 +139,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
|
||||
StatusViewComponent,
|
||||
FeesBoxComponent,
|
||||
DifficultyComponent,
|
||||
TxBowtieGraphComponent,
|
||||
TermsOfServiceComponent,
|
||||
PrivacyPolicyComponent,
|
||||
TrademarkPolicyComponent,
|
||||
@ -242,6 +244,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
|
||||
StatusViewComponent,
|
||||
FeesBoxComponent,
|
||||
DifficultyComponent,
|
||||
TxBowtieGraphComponent,
|
||||
TermsOfServiceComponent,
|
||||
PrivacyPolicyComponent,
|
||||
TrademarkPolicyComponent,
|
||||
|
Loading…
Reference in New Issue
Block a user