vin/vout selection syntax via url fragments

This commit is contained in:
Mononaut 2022-10-11 20:54:17 +00:00
parent ee6766e34c
commit ae9439a991
No known key found for this signature in database
GPG Key ID: 61B952CAF4838F94
5 changed files with 74 additions and 29 deletions

View File

@ -210,8 +210,6 @@
[network]="network" [network]="network"
[tooltip]="true" [tooltip]="true"
[inputIndex]="inputIndex" [outputIndex]="outputIndex" [inputIndex]="inputIndex" [outputIndex]="outputIndex"
(selectInput)="selectInput($event)"
(selectOutput)="selectOutput($event)"
> >
</tx-bowtie-graph> </tx-bowtie-graph>
</div> </div>

View File

@ -18,6 +18,7 @@ import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { BlockExtended, CpfpInfo } from '../../interfaces/node-api.interface'; import { BlockExtended, CpfpInfo } from '../../interfaces/node-api.interface';
import { LiquidUnblinding } from './liquid-ublinding'; import { LiquidUnblinding } from './liquid-ublinding';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
@Component({ @Component({
selector: 'app-transaction', selector: 'app-transaction',
@ -40,6 +41,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
txReplacedSubscription: Subscription; txReplacedSubscription: Subscription;
blocksSubscription: Subscription; blocksSubscription: Subscription;
queryParamsSubscription: Subscription; queryParamsSubscription: Subscription;
urlFragmentSubscription: Subscription;
fragmentParams: URLSearchParams;
rbfTransaction: undefined | Transaction; rbfTransaction: undefined | Transaction;
cpfpInfo: CpfpInfo | null; cpfpInfo: CpfpInfo | null;
showCpfpDetails = false; showCpfpDetails = false;
@ -67,6 +70,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private relativeUrlPipe: RelativeUrlPipe,
private electrsApiService: ElectrsApiService, private electrsApiService: ElectrsApiService,
private stateService: StateService, private stateService: StateService,
private websocketService: WebsocketService, private websocketService: WebsocketService,
@ -93,6 +97,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
map((da) => da.timeAvg) map((da) => da.timeAvg)
); );
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
this.fragmentParams = new URLSearchParams(fragment || '');
const vin = parseInt(this.fragmentParams.get('vin'), 10);
const vout = parseInt(this.fragmentParams.get('vout'), 10);
this.inputIndex = (!isNaN(vin) && vin >= 0) ? vin : null;
this.outputIndex = (!isNaN(vout) && vout >= 0) ? vout : null;
});
this.fetchCpfpSubscription = this.fetchCpfp$ this.fetchCpfpSubscription = this.fetchCpfp$
.pipe( .pipe(
switchMap((txId) => switchMap((txId) =>
@ -132,13 +144,29 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
switchMap((params: ParamMap) => { switchMap((params: ParamMap) => {
const urlMatch = (params.get('id') || '').split(':'); const urlMatch = (params.get('id') || '').split(':');
if (urlMatch.length === 2 && urlMatch[1].length === 64) { if (urlMatch.length === 2 && urlMatch[1].length === 64) {
this.inputIndex = parseInt(urlMatch[0], 10); const vin = parseInt(urlMatch[0], 10);
this.outputIndex = null;
this.txId = urlMatch[1]; this.txId = urlMatch[1];
// rewrite legacy vin syntax
if (!isNaN(vin)) {
this.fragmentParams.set('vin', vin.toString());
this.fragmentParams.delete('vout');
}
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.txId], {
queryParamsHandling: 'merge',
fragment: this.fragmentParams.toString(),
});
} else { } else {
this.txId = urlMatch[0]; this.txId = urlMatch[0];
this.outputIndex = urlMatch[1] === undefined ? null : parseInt(urlMatch[1], 10); const vout = parseInt(urlMatch[1], 10);
this.inputIndex = null; if (urlMatch.length > 1 && !isNaN(vout)) {
// rewrite legacy vout syntax
this.fragmentParams.set('vout', vout.toString());
this.fragmentParams.delete('vin');
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.txId], {
queryParamsHandling: 'merge',
fragment: this.fragmentParams.toString(),
});
}
} }
this.seoService.setTitle( this.seoService.setTitle(
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:` $localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
@ -222,6 +250,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.fetchCpfp$.next(this.tx.txid); this.fetchCpfp$.next(this.tx.txid);
} }
} }
setTimeout(() => { this.applyFragment(); }, 0);
}, },
(error) => { (error) => {
this.error = error; this.error = error;
@ -359,14 +388,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.graphExpanded = false; this.graphExpanded = false;
} }
selectInput(input) { // simulate normal anchor fragment behavior
this.inputIndex = input; applyFragment(): void {
this.outputIndex = null; const anchor = Array.from(this.fragmentParams.entries()).find(([frag, value]) => value === '');
} if (anchor) {
const anchorElement = document.getElementById(anchor[0]);
selectOutput(output) { if (anchorElement) {
this.outputIndex = output; anchorElement.scrollIntoView();
this.inputIndex = null; }
}
} }
@HostListener('window:resize', ['$event']) @HostListener('window:resize', ['$event'])
@ -383,6 +413,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.blocksSubscription.unsubscribe(); this.blocksSubscription.unsubscribe();
this.queryParamsSubscription.unsubscribe(); this.queryParamsSubscription.unsubscribe();
this.flowPrefSubscription.unsubscribe(); this.flowPrefSubscription.unsubscribe();
this.urlFragmentSubscription.unsubscribe();
this.leaveTransaction(); this.leaveTransaction();
} }
} }

View File

@ -43,7 +43,7 @@
</ng-template> </ng-template>
</ng-template> </ng-template>
<ng-template #defaultPrevout> <ng-template #defaultPrevout>
<a [routerLink]="['/tx/' | relativeUrl, vin.txid + ':' + vin.vout]" class="red"> <a [routerLink]="['/tx/' | relativeUrl, vin.txid]" [fragment]="'vout=' + vin.vout" class="red">
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon> <fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
</a> </a>
</ng-template> </ng-template>
@ -220,7 +220,7 @@
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon> <fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
</span> </span>
<ng-template #spent> <ng-template #spent>
<a *ngIf="tx._outspends[vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, tx._outspends[vindex].vin + ':' + tx._outspends[vindex].txid]" class="red"> <a *ngIf="tx._outspends[vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, tx._outspends[vindex].txid]" [fragment]="'vin=' + tx._outspends[vindex].vin" class="red">
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon> <fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
</a> </a>
<ng-template #outputNoTxId> <ng-template #outputNoTxId>

View File

@ -1,4 +1,4 @@
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, HostListener } from '@angular/core'; import { Component, OnInit, Input, OnChanges, HostListener } from '@angular/core';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { Outspend, Transaction } from '../../interfaces/electrs.interface'; import { Outspend, Transaction } from '../../interfaces/electrs.interface';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@ -43,9 +43,6 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
@Input() inputIndex: number; @Input() inputIndex: number;
@Input() outputIndex: number; @Input() outputIndex: number;
@Output() selectInput = new EventEmitter<number>();
@Output() selectOutput = new EventEmitter<number>();
inputData: Xput[]; inputData: Xput[];
outputData: Xput[]; outputData: Xput[];
inputs: SvgLine[]; inputs: SvgLine[];
@ -369,23 +366,41 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
if (side === 'input') { if (side === 'input') {
const input = this.tx.vin[index]; const input = this.tx.vin[index];
if (input && input.txid && input.vout != null) { if (input && input.txid && input.vout != null) {
this.router.navigate([this.relativeUrlPipe.transform('/tx'), input.txid + ':' + input.vout], { this.router.navigate([this.relativeUrlPipe.transform('/tx'), input.txid], {
queryParamsHandling: 'merge', queryParamsHandling: 'merge',
fragment: 'flow' fragment: (new URLSearchParams({
flow: '',
vout: input.vout.toString(),
})).toString(),
});
} else if (index != null) {
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.tx.txid], {
queryParamsHandling: 'merge',
fragment: (new URLSearchParams({
flow: '',
vin: index.toString(),
})).toString(),
}); });
} else {
this.selectInput.emit(index);
} }
} else { } else {
const output = this.tx.vout[index]; const output = this.tx.vout[index];
const outspend = this.outspends[index]; const outspend = this.outspends[index];
if (output && outspend && outspend.spent && outspend.txid) { if (output && outspend && outspend.spent && outspend.txid) {
this.router.navigate([this.relativeUrlPipe.transform('/tx'), outspend.vin + ':' + outspend.txid], { this.router.navigate([this.relativeUrlPipe.transform('/tx'), outspend.txid], {
queryParamsHandling: 'merge', queryParamsHandling: 'merge',
fragment: 'flow' fragment: (new URLSearchParams({
flow: '',
vin: outspend.vin.toString(),
})).toString(),
});
} else if (index != null) {
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.tx.txid], {
queryParamsHandling: 'merge',
fragment: (new URLSearchParams({
flow: '',
vout: index.toString(),
})).toString(),
}); });
} else {
this.selectOutput.emit(index);
} }
} }
} }

View File

@ -16,7 +16,8 @@
], ],
"lib": [ "lib": [
"es2018", "es2018",
"dom" "dom",
"dom.iterable"
] ]
}, },
"angularCompilerOptions": { "angularCompilerOptions": {