Merge pull request #2649 from mononaut/flow-diagram-spent-connectors

Extend flow diagram to differentiate spent and unspent TXOs
This commit is contained in:
wiz 2022-11-22 13:52:22 +09:00 committed by GitHub
commit 9345b1609f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 188 additions and 38 deletions

View File

@ -126,9 +126,13 @@ export class LiquidUnblinding {
}
async checkUnblindedTx(tx: Transaction) {
const windowLocationHash = window.location.hash.substring('#blinded='.length);
if (windowLocationHash.length > 0) {
const blinders = this.parseBlinders(windowLocationHash);
if (!window.location.hash?.length) {
return tx;
}
const fragmentParams = new URLSearchParams(window.location.hash.slice(1) || '');
const blinderStr = fragmentParams.get('blinded');
if (blinderStr && blinderStr.length) {
const blinders = this.parseBlinders(blinderStr);
if (blinders) {
this.commitments = await this.makeCommitmentMap(blinders);
return this.tryUnblindTx(tx);

View File

@ -29,7 +29,7 @@
<div class="row graph-wrapper">
<tx-bowtie-graph [tx]="tx" [width]="1112" [height]="346" [network]="network"></tx-bowtie-graph>
<tx-bowtie-graph [tx]="tx" [width]="1132" [height]="346" [network]="network"></tx-bowtie-graph>
<div class="above-bow">
<p class="field pair">
<span [innerHTML]="'&lrm;' + (tx.size | bytes: 2)"></span>

View File

@ -69,7 +69,7 @@
.graph-wrapper {
position: relative;
background: #181b2d;
padding: 10px;
padding: 10px 0;
padding-bottom: 0;
.above-bow {

View File

@ -209,6 +209,7 @@
[maxStrands]="graphExpanded ? maxInOut : 24"
[network]="network"
[tooltip]="true"
[connectors]="true"
[inputIndex]="inputIndex" [outputIndex]="outputIndex"
>
</tx-bowtie-graph>

View File

@ -86,7 +86,7 @@
position: relative;
width: 100%;
background: #181b2d;
padding: 10px;
padding: 10px 0;
padding-bottom: 0;
}

View File

@ -404,7 +404,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
@HostListener('window:resize', ['$event'])
setGraphSize(): void {
if (this.graphContainer) {
this.graphWidth = this.graphContainer.nativeElement.clientWidth - 24;
this.graphWidth = this.graphContainer.nativeElement.clientWidth;
}
}

View File

@ -22,13 +22,13 @@
<ng-template #pegin>
<ng-container *ngIf="line.pegin; else pegout">
<p>Peg In</p>
<p *ngIf="!isConnector">Peg In</p>
</ng-container>
</ng-template>
<ng-template #pegout>
<ng-container *ngIf="line.pegout; else normal">
<p>Peg Out</p>
<p *ngIf="!isConnector">Peg Out</p>
<p *ngIf="line.value != null"><app-amount [satoshis]="line.value"></app-amount></p>
<p class="address">
<span class="first">{{ line.pegout.slice(0, -4) }}</span>
@ -38,7 +38,7 @@
</ng-template>
<ng-template #normal>
<p>
<p *ngIf="!isConnector">
<ng-container [ngSwitch]="line.type">
<span *ngSwitchCase="'input'" i18n="transaction.input">Input</span>
<span *ngSwitchCase="'output'" i18n="transaction.output">Output</span>
@ -46,6 +46,17 @@
</ng-container>
<span *ngIf="line.type !== 'fee'"> #{{ line.index + 1 }}</span>
</p>
<ng-container *ngIf="isConnector && line.txid">
<p>
<span i18n="transaction">Transaction</span>&nbsp;
<span class="first">{{ line.txid.slice(0, 8) }}</span>...
<span class="last-four">{{ line.txid.slice(-4) }}</span>
</p>
<ng-container [ngSwitch]="line.type">
<p *ngSwitchCase="'input'"><span i18n="transaction.output">Output</span>&nbsp; #{{ line.vout + 1 }}</p>
<p *ngSwitchCase="'output'"><span i18n="transaction.input">Input</span>&nbsp; #{{ line.vin + 1 }}</p>
</ng-container>
</ng-container>
<p *ngIf="line.value == null && line.confidential" i18n="shared.confidential">Confidential</p>
<p *ngIf="line.value != null"><app-amount [satoshis]="line.value"></app-amount></p>
<p *ngIf="line.type !== 'fee' && line.address" class="address">

View File

@ -5,6 +5,9 @@ interface Xput {
type: 'input' | 'output' | 'fee';
value?: number;
index?: number;
txid?: string;
vin?: number;
vout?: number;
address?: string;
rest?: number;
coinbase?: boolean;
@ -21,6 +24,7 @@ interface Xput {
export class TxBowtieGraphTooltipComponent implements OnChanges {
@Input() line: Xput | void;
@Input() cursorPosition: { x: number, y: number };
@Input() isConnector: boolean = false;
tooltipPosition = { x: 0, y: 0 };

View File

@ -29,6 +29,14 @@
<stop offset="0%" [attr.stop-color]="gradient[1]" />
<stop offset="100%" [attr.stop-color]="gradient[0]" />
</linearGradient>
<linearGradient id="input-connector-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" [attr.stop-color]="gradient[2]" />
<stop offset="80%" [attr.stop-color]="gradient[0]" />
</linearGradient>
<linearGradient id="output-connector-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="20%" [attr.stop-color]="gradient[0]" />
<stop offset="100%" [attr.stop-color]="gradient[2]" />
</linearGradient>
<linearGradient id="input-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" [attr.stop-color]="gradient[0]" />
<stop offset="2%" [attr.stop-color]="gradient[0]" />
@ -41,6 +49,14 @@
<stop offset="98%" [attr.stop-color]="gradient[0]" />
<stop offset="100%" [attr.stop-color]="gradient[0]" />
</linearGradient>
<linearGradient id="input-hover-connector-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="white" />
<stop offset="80%" [attr.stop-color]="gradient[0]" />
</linearGradient>
<linearGradient id="output-hover-connector-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="20%" [attr.stop-color]="gradient[0]" />
<stop offset="100%" stop-color="white" />
</linearGradient>
<linearGradient id="input-highlight-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" [attr.stop-color]="gradient[0]" />
<stop offset="2%" [attr.stop-color]="gradient[0]" />
@ -65,6 +81,22 @@
</defs>
<path [attr.d]="middle.path" class="line middle" [style]="middle.style"/>
<ng-container *ngFor="let input of inputs; let i = index">
<path *ngIf="connectors && !inputData[i].coinbase && !inputData[i].pegin"
[attr.d]="input.connectorPath"
class="input connector {{input.class}}"
[class.highlight]="inputData[i].index === inputIndex"
(pointerover)="onHover($event, 'input-connector', i);"
(pointerout)="onBlur($event, 'input-connector', i);"
(click)="onClick($event, 'input-connector', inputData[i].index);"
/>
<path
[attr.d]="input.markerPath"
class="input marker-target {{input.class}}"
[class.highlight]="inputData[i].index === inputIndex"
(pointerover)="onHover($event, 'input', i);"
(pointerout)="onBlur($event, 'input', i);"
(click)="onClick($event, 'input', inputData[i].index);"
/>
<path
[attr.d]="input.path"
class="line {{input.class}}"
@ -77,6 +109,22 @@
/>
</ng-container>
<ng-container *ngFor="let output of outputs; let i = index">
<path *ngIf="connectors && outspends[outputData[i].index]?.spent"
[attr.d]="output.connectorPath"
class="output connector {{output.class}}"
[class.highlight]="outputData[i].index === outputIndex"
(pointerover)="onHover($event, 'output-connector', i);"
(pointerout)="onBlur($event, 'output-connector', i);"
(click)="onClick($event, 'output-connector', outputData[i].index);"
/>
<path
[attr.d]="output.markerPath"
class="output marker-target {{output.class}}"
[class.highlight]="outputData[i].index === outputIndex"
(pointerover)="onHover($event, 'output', i);"
(pointerout)="onBlur($event, 'output', i);"
(click)="onClick($event, 'output', outputData[i].index);"
/>
<path
[attr.d]="output.path"
class="line {{output.class}}"
@ -94,5 +142,6 @@
*ngIf=[tooltip]
[line]="hoverLine"
[cursorPosition]="tooltipPosition"
[isConnector]="hoverConnector"
></app-tx-bowtie-graph-tooltip>
</div>

View File

@ -22,19 +22,46 @@
stroke: url(#output-highlight-gradient);
}
}
}
&:hover {
z-index: 10;
cursor: pointer;
&.input {
stroke: url(#input-hover-gradient);
}
&.output {
stroke: url(#output-hover-gradient);
}
&.fee {
stroke: url(#fee-hover-gradient);
}
.line:hover, .marker-target:hover + .line {
z-index: 10;
cursor: pointer;
&.input {
stroke: url(#input-hover-gradient);
}
&.output {
stroke: url(#output-hover-gradient);
}
&.fee {
stroke: url(#fee-hover-gradient);
}
}
}
.connector {
stroke: none;
opacity: 0.75;
cursor: pointer;
&.input {
fill: url(#input-connector-gradient);
}
&.output {
fill: url(#output-connector-gradient);
}
}
.connector:hover {
&.input {
fill: url(#input-hover-connector-gradient);
}
&.output {
fill: url(#output-hover-connector-gradient);
}
}
.marker-target {
stroke: none;
fill: transparent;
cursor: pointer;
}
}

View File

@ -11,12 +11,17 @@ interface SvgLine {
path: string;
style: string;
class?: string;
connectorPath?: string;
markerPath?: string;
}
interface Xput {
type: 'input' | 'output' | 'fee';
value?: number;
index?: number;
txid?: string;
vin?: number;
vout?: number;
address?: string;
rest?: number;
coinbase?: boolean;
@ -40,6 +45,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
@Input() minWeight = 2; //
@Input() maxStrands = 24; // number of inputs/outputs to keep fully on-screen.
@Input() tooltip = false;
@Input() connectors = false;
@Input() inputIndex: number;
@Input() outputIndex: number;
@ -49,9 +55,12 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
outputs: SvgLine[];
middle: SvgLine;
midWidth: number;
txWidth: number;
connectorWidth: number;
combinedWeight: number;
isLiquid: boolean = false;
hoverLine: Xput | void = null;
hoverConnector: boolean = false;
tooltipPosition = { x: 0, y: 0 };
outspends: Outspend[] = [];
@ -59,16 +68,16 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
refreshOutspends$: ReplaySubject<string> = new ReplaySubject();
gradientColors = {
'': ['#9339f4', '#105fb0'],
bisq: ['#9339f4', '#105fb0'],
'': ['#9339f4', '#105fb0', '#9339f400'],
bisq: ['#9339f4', '#105fb0', '#9339f400'],
// liquid: ['#116761', '#183550'],
liquid: ['#09a197', '#0f62af'],
liquid: ['#09a197', '#0f62af', '#09a19700'],
// 'liquidtestnet': ['#494a4a', '#272e46'],
'liquidtestnet': ['#d2d2d2', '#979797'],
'liquidtestnet': ['#d2d2d2', '#979797', '#d2d2d200'],
// testnet: ['#1d486f', '#183550'],
testnet: ['#4edf77', '#10a0af'],
testnet: ['#4edf77', '#10a0af', '#4edf7700'],
// signet: ['#6f1d5d', '#471850'],
signet: ['#d24fc8', '#a84fd2'],
signet: ['#d24fc8', '#a84fd2', '#d24fc800'],
};
gradient: string[] = ['#105fb0', '#105fb0'];
@ -118,7 +127,9 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet');
this.gradient = this.gradientColors[this.network];
this.midWidth = Math.min(10, Math.ceil(this.width / 100));
this.combinedWeight = Math.min(this.maxCombinedWeight, Math.floor((this.width - (2 * this.midWidth)) / 6));
this.txWidth = this.connectors ? Math.max(this.width - 200, this.width * 0.8) : this.width - 20;
this.combinedWeight = Math.min(this.maxCombinedWeight, Math.floor((this.txWidth - (2 * this.midWidth)) / 6));
this.connectorWidth = (this.width - this.txWidth) / 2;
const totalValue = this.calcTotalValue(this.tx);
let voutWithFee = this.tx.vout.map((v, i) => {
@ -141,6 +152,8 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
return {
type: 'input',
value: v?.prevout?.value,
txid: v.txid,
vout: v.vout,
address: v?.prevout?.scriptpubkey_address || v?.prevout?.scriptpubkey_type?.toUpperCase(),
index: i,
coinbase: v?.is_coinbase,
@ -268,7 +281,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
// required to prevent this line overlapping its neighbor
if (this.tooltip || !xputs[i].rest) {
const w = (this.width - Math.max(lastWeight, line.weight)) / 2; // approximate horizontal width of the curved section of the line
const w = (this.width - Math.max(lastWeight, line.weight) - (2 * this.connectorWidth)) / 2; // approximate horizontal width of the curved section of the line
const y1 = line.outerY;
const y2 = line.innerY;
const t = (lastWeight + line.weight) / 2; // distance between center of this line and center of previous line
@ -308,13 +321,15 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
return {
path: this.makePath(side, line.outerY, line.innerY, line.thickness, line.offset, pad + maxOffset),
style: this.makeStyle(line.thickness, xputs[i].type),
class: xputs[i].type
class: xputs[i].type,
connectorPath: this.connectors ? this.makeConnectorPath(side, line.outerY, line.innerY, line.thickness): null,
markerPath: this.makeMarkerPath(side, line.outerY, line.innerY, line.thickness),
};
});
}
makePath(side: 'in' | 'out', outer: number, inner: number, weight: number, offset: number, pad: number): string {
const start = (weight * 0.5);
const start = (weight * 0.5) + this.connectorWidth;
const curveStart = Math.max(start + 1, pad - offset);
const end = this.width / 2 - (this.midWidth * 0.9) + 1;
const curveEnd = end - offset - 10;
@ -332,6 +347,40 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
}
}
makeConnectorPath(side: 'in' | 'out', y: number, inner, weight: number): string {
const halfWidth = weight * 0.5;
const offset = 10; //Math.max(2, halfWidth * 0.2);
const lineEnd = this.connectorWidth;
// align with for svg horizontal gradient bug correction
if (Math.round(y) === Math.round(inner)) {
y -= 1;
}
if (side === 'in') {
return `M ${lineEnd - offset} ${y - halfWidth} L ${halfWidth + lineEnd - offset} ${y} L ${lineEnd - offset} ${y + halfWidth} L -${10} ${ y + halfWidth} L -${10} ${y - halfWidth}`;
} else {
return `M ${this.width - halfWidth - lineEnd + offset} ${y - halfWidth} L ${this.width - lineEnd + offset} ${y} L ${this.width - halfWidth - lineEnd + offset} ${y + halfWidth} L ${this.width + 10} ${ y + halfWidth} L ${this.width + 10} ${y - halfWidth}`;
}
}
makeMarkerPath(side: 'in' | 'out', y: number, inner, weight: number): string {
const halfWidth = weight * 0.5;
const offset = 10; //Math.max(2, halfWidth * 0.2);
const lineEnd = this.connectorWidth;
// align with for svg horizontal gradient bug correction
if (Math.round(y) === Math.round(inner)) {
y -= 1;
}
if (side === 'in') {
return `M ${lineEnd - offset} ${y - halfWidth} L ${halfWidth + lineEnd - offset} ${y} L ${lineEnd - offset} ${y + halfWidth} L ${weight + lineEnd} ${ y + halfWidth} L ${weight + lineEnd} ${y - halfWidth}`;
} else {
return `M ${this.width - halfWidth - lineEnd + offset} ${y - halfWidth} L ${this.width - lineEnd + offset} ${y} L ${this.width - halfWidth - lineEnd + offset} ${y + halfWidth} L ${this.width - halfWidth - lineEnd} ${ y + halfWidth} L ${this.width - halfWidth - lineEnd} ${y - halfWidth}`;
}
}
makeStyle(minWeight, type): string {
if (type === 'fee') {
return `stroke-width: ${minWeight}`;
@ -346,26 +395,31 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
}
onHover(event, side, index): void {
if (side === 'input') {
if (side.startsWith('input')) {
this.hoverLine = {
...this.inputData[index],
index
};
this.hoverConnector = (side === 'input-connector');
} else {
this.hoverLine = {
...this.outputData[index]
...this.outputData[index],
...this.outspends[this.outputData[index].index]
};
this.hoverConnector = (side === 'output-connector');
}
}
onBlur(event, side, index): void {
this.hoverLine = null;
this.hoverConnector = false;
}
onClick(event, side, index): void {
if (side === 'input') {
if (side.startsWith('input')) {
const input = this.tx.vin[index];
if (input && !input.is_coinbase && !input.is_pegin && input.txid && input.vout != null) {
if (side === 'input-connector' && input && !input.is_coinbase && !input.is_pegin && input.txid && input.vout != null) {
this.router.navigate([this.relativeUrlPipe.transform('/tx'), input.txid], {
queryParamsHandling: 'merge',
fragment: (new URLSearchParams({
@ -385,7 +439,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
} else {
const output = this.tx.vout[index];
const outspend = this.outspends[index];
if (output && outspend && outspend.spent && outspend.txid) {
if (side === 'output-connector' && output && outspend && outspend.spent && outspend.txid) {
this.router.navigate([this.relativeUrlPipe.transform('/tx'), outspend.txid], {
queryParamsHandling: 'merge',
fragment: (new URLSearchParams({