Merge branch 'master' into rtl-language-time-default
This commit is contained in:
		
						commit
						be92b708c2
					
				@ -25,6 +25,8 @@ export class AppComponent implements OnInit {
 | 
			
		||||
    if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
 | 
			
		||||
      this.dir = 'rtl';
 | 
			
		||||
      this.class = 'rtl-layout';
 | 
			
		||||
    } else {
 | 
			
		||||
      this.class = 'ltr-layout';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    tooltipConfig.animation = false;
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,6 @@
 | 
			
		||||
  left: 0;
 | 
			
		||||
  top: 75px;
 | 
			
		||||
  transform: translateX(50vw);
 | 
			
		||||
  transition: transform 1s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.position-container.liquid, .position-container.liquidtestnet {
 | 
			
		||||
@ -84,9 +83,9 @@
 | 
			
		||||
 | 
			
		||||
.time-toggle {
 | 
			
		||||
  color: white;
 | 
			
		||||
  font-size: 1rem;
 | 
			
		||||
  font-size: 0.8rem;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  bottom: -1.5em;
 | 
			
		||||
  bottom: -1.8em;
 | 
			
		||||
  left: 1px;
 | 
			
		||||
  transform: translateX(-50%);
 | 
			
		||||
  background: none;
 | 
			
		||||
@ -97,14 +96,31 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blockchain-wrapper.ltr-transition .blocks-wrapper,
 | 
			
		||||
.blockchain-wrapper.ltr-transition .position-container,
 | 
			
		||||
.blockchain-wrapper.ltr-transition .time-toggle {
 | 
			
		||||
  transition: transform 1s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blockchain-wrapper.time-ltr .blocks-wrapper {
 | 
			
		||||
  transform: scaleX(-1);
 | 
			
		||||
.blockchain-wrapper.time-ltr {
 | 
			
		||||
  .blocks-wrapper {
 | 
			
		||||
    transform: scaleX(-1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .time-toggle {
 | 
			
		||||
    transform: translateX(-50%) scaleX(-1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blockchain-wrapper.time-ltr .time-toggle {
 | 
			
		||||
  transform: translateX(-50%) scaleX(-1);
 | 
			
		||||
:host-context(.ltr-layout) {
 | 
			
		||||
  .blockchain-wrapper.time-ltr .blocks-wrapper,
 | 
			
		||||
  .blockchain-wrapper .blocks-wrapper {
 | 
			
		||||
    direction: ltr;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context(.rtl-layout) {
 | 
			
		||||
  .blockchain-wrapper.time-ltr .blocks-wrapper,
 | 
			
		||||
  .blockchain-wrapper .blocks-wrapper {
 | 
			
		||||
    direction: rtl;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -146,4 +146,10 @@
 | 
			
		||||
  .block-body {
 | 
			
		||||
    transform: scaleX(-1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context(.rtl-layout) {
 | 
			
		||||
  #arrow-up {
 | 
			
		||||
    transform: translateX(70px);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -8,7 +8,7 @@
 | 
			
		||||
 | 
			
		||||
<div *ngIf="countdown > 0" class="warning-label">{{ eventName }} in {{ countdown | number }} block{{ countdown === 1 ? '' : 's' }}!</div>
 | 
			
		||||
 | 
			
		||||
<div id="blockchain-container" dir="ltr" #blockchainContainer
 | 
			
		||||
<div id="blockchain-container" [dir]="timeLtr ? 'rtl' : 'ltr'" #blockchainContainer
 | 
			
		||||
  (mousedown)="onMouseDown($event)"
 | 
			
		||||
  (dragstart)="onDragStart($event)"
 | 
			
		||||
>
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core';
 | 
			
		||||
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild } from '@angular/core';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { specialBlocks } from '../../app.constants';
 | 
			
		||||
 | 
			
		||||
@ -7,7 +8,7 @@ import { specialBlocks } from '../../app.constants';
 | 
			
		||||
  templateUrl: './start.component.html',
 | 
			
		||||
  styleUrls: ['./start.component.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class StartComponent implements OnInit {
 | 
			
		||||
export class StartComponent implements OnInit, OnDestroy {
 | 
			
		||||
  interval = 60;
 | 
			
		||||
  colors = ['#5E35B1', '#ffffff'];
 | 
			
		||||
 | 
			
		||||
@ -16,6 +17,8 @@ export class StartComponent implements OnInit {
 | 
			
		||||
  eventName = '';
 | 
			
		||||
  mouseDragStartX: number;
 | 
			
		||||
  blockchainScrollLeftInit: number;
 | 
			
		||||
  timeLtrSubscription: Subscription;
 | 
			
		||||
  timeLtr: boolean = this.stateService.timeLtr.value;
 | 
			
		||||
  @ViewChild('blockchainContainer') blockchainContainer: ElementRef;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
@ -23,6 +26,9 @@ export class StartComponent implements OnInit {
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
 | 
			
		||||
      this.timeLtr = !!ltr;
 | 
			
		||||
    });
 | 
			
		||||
    this.stateService.blocks$
 | 
			
		||||
      .subscribe((blocks: any) => {
 | 
			
		||||
        if (this.stateService.network !== '') {
 | 
			
		||||
@ -72,4 +78,8 @@ export class StartComponent implements OnInit {
 | 
			
		||||
    this.mouseDragStartX = null;
 | 
			
		||||
    this.stateService.setBlockScrollingInProgress(false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
    this.timeLtrSubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -195,7 +195,7 @@
 | 
			
		||||
        <h2 id="flow" i18n="transaction.flow|Transaction flow">Flow</h2>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <button type="button" class="btn btn-outline-info flow-toggle btn-sm float-right" (click)="toggleGraph()" i18n="hide-flow-diagram">Hide flow diagram</button>
 | 
			
		||||
      <button type="button" class="btn btn-outline-info flow-toggle btn-sm float-right" (click)="toggleGraph()" i18n="hide-diagram">Hide diagram</button>
 | 
			
		||||
 | 
			
		||||
      <div class="clearfix"></div>
 | 
			
		||||
 | 
			
		||||
@ -208,7 +208,11 @@
 | 
			
		||||
            [lineLimit]="inOutLimit"
 | 
			
		||||
            [maxStrands]="graphExpanded ? maxInOut : 24"
 | 
			
		||||
            [network]="network"
 | 
			
		||||
            [tooltip]="true">
 | 
			
		||||
            [tooltip]="true"
 | 
			
		||||
            [inputIndex]="inputIndex" [outputIndex]="outputIndex"
 | 
			
		||||
            (selectInput)="selectInput($event)"
 | 
			
		||||
            (selectOutput)="selectOutput($event)"
 | 
			
		||||
          >
 | 
			
		||||
          </tx-bowtie-graph>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="toggle-wrapper" *ngIf="maxInOut > 24">
 | 
			
		||||
@ -234,13 +238,13 @@
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="title-buttons">
 | 
			
		||||
        <button *ngIf="!showFlow" type="button" class="btn btn-outline-info flow-toggle btn-sm" (click)="toggleGraph()" i18n="show">Show flow diagram</button>
 | 
			
		||||
        <button *ngIf="!showFlow" type="button" class="btn btn-outline-info flow-toggle btn-sm" (click)="toggleGraph()" i18n="show-diagram">Show diagram</button>
 | 
			
		||||
        <button type="button" class="btn btn-outline-info btn-sm" (click)="txList.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <app-transactions-list #txList [transactions]="[tx]" [errorUnblinded]="errorUnblinded" [outputIndex]="outputIndex" [transactionPage]="true"></app-transactions-list>
 | 
			
		||||
    <app-transactions-list #txList [transactions]="[tx]" [errorUnblinded]="errorUnblinded" [inputIndex]="inputIndex" [outputIndex]="outputIndex" [transactionPage]="true"></app-transactions-list>
 | 
			
		||||
 | 
			
		||||
    <div class="title text-left">
 | 
			
		||||
      <h2 i18n="transaction.details">Details</h2>
 | 
			
		||||
 | 
			
		||||
@ -3,38 +3,38 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.container-buttons {
 | 
			
		||||
  align-self: center;
 | 
			
		||||
  align-self: flex-start;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.title-block {
 | 
			
		||||
	flex-wrap: wrap;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  align-items: baseline;
 | 
			
		||||
  @media (min-width: 650px) {
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
  }
 | 
			
		||||
  h1 {
 | 
			
		||||
    margin: 0rem;
 | 
			
		||||
    margin-right: 15px;
 | 
			
		||||
    line-height: 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.tx-link {
 | 
			
		||||
  display: flex;
 | 
			
		||||
	flex-grow: 1;
 | 
			
		||||
  margin-bottom: 0px;
 | 
			
		||||
  margin-top: 8px;
 | 
			
		||||
	@media (min-width: 650px) {
 | 
			
		||||
    align-self: end;
 | 
			
		||||
    margin-left: 15px;
 | 
			
		||||
    margin-top: 0px;
 | 
			
		||||
    margin-bottom: -3px;
 | 
			
		||||
	}
 | 
			
		||||
	@media (min-width: 768px) {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  flex-shrink: 0;
 | 
			
		||||
  @media (min-width: 651px) {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    width: auto;
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
    margin-bottom: 0px;
 | 
			
		||||
    top: 1px;
 | 
			
		||||
    position: relative;
 | 
			
		||||
	}
 | 
			
		||||
	@media (max-width: 768px) {
 | 
			
		||||
	  order: 3;
 | 
			
		||||
	}
 | 
			
		||||
  }
 | 
			
		||||
  @media (max-width: 650px) {
 | 
			
		||||
    order: 3;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.td-width {
 | 
			
		||||
 | 
			
		||||
@ -47,6 +47,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
  now = new Date().getTime();
 | 
			
		||||
  timeAvg$: Observable<number>;
 | 
			
		||||
  liquidUnblinding = new LiquidUnblinding();
 | 
			
		||||
  inputIndex: number;
 | 
			
		||||
  outputIndex: number;
 | 
			
		||||
  showFlow: boolean = true;
 | 
			
		||||
  graphExpanded: boolean = false;
 | 
			
		||||
@ -121,8 +122,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
      .pipe(
 | 
			
		||||
        switchMap((params: ParamMap) => {
 | 
			
		||||
          const urlMatch = (params.get('id') || '').split(':');
 | 
			
		||||
          this.txId = urlMatch[0];
 | 
			
		||||
          this.outputIndex = urlMatch[1] === undefined ? null : parseInt(urlMatch[1], 10);
 | 
			
		||||
          if (urlMatch.length === 2 && urlMatch[1].length === 64) {
 | 
			
		||||
            this.inputIndex = parseInt(urlMatch[0], 10);
 | 
			
		||||
            this.outputIndex = null;
 | 
			
		||||
            this.txId = urlMatch[1];
 | 
			
		||||
          } else {
 | 
			
		||||
            this.txId = urlMatch[0];
 | 
			
		||||
            this.outputIndex = urlMatch[1] === undefined ? null : parseInt(urlMatch[1], 10);
 | 
			
		||||
            this.inputIndex = null;
 | 
			
		||||
          }
 | 
			
		||||
          this.seoService.setTitle(
 | 
			
		||||
            $localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
 | 
			
		||||
          );
 | 
			
		||||
@ -334,6 +342,16 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
    this.graphExpanded = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  selectInput(input) {
 | 
			
		||||
    this.inputIndex = input;
 | 
			
		||||
    this.outputIndex = null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  selectOutput(output) {
 | 
			
		||||
    this.outputIndex = output;
 | 
			
		||||
    this.inputIndex = null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @HostListener('window:resize', ['$event'])
 | 
			
		||||
  setGraphSize(): void {
 | 
			
		||||
    if (this.graphContainer) {
 | 
			
		||||
 | 
			
		||||
@ -20,9 +20,9 @@
 | 
			
		||||
      <div class="col">
 | 
			
		||||
        <table class="table table-borderless smaller-text table-sm table-tx-vin">
 | 
			
		||||
          <tbody>
 | 
			
		||||
            <ng-template ngFor let-vin let-vindex="index" [ngForOf]="tx['@vinLimit'] ? ((tx.vin.length > rowLimit) ? tx.vin.slice(0, rowLimit - 2) : tx.vin.slice(0, rowLimit)) : tx.vin" [ngForTrackBy]="trackByIndexFn">
 | 
			
		||||
            <ng-template ngFor let-vin let-vindex="index" [ngForOf]="tx['@vinLimit'] ? ((tx.vin.length > inputRowLimit) ? tx.vin.slice(0, inputRowLimit - 2) : tx.vin.slice(0, inputRowLimit)) : tx.vin" [ngForTrackBy]="trackByIndexFn">
 | 
			
		||||
              <tr [ngClass]="{
 | 
			
		||||
                'assetBox': assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded,
 | 
			
		||||
                'assetBox': (assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded) || inputIndex === vindex,
 | 
			
		||||
                'highlight': vin.prevout?.scriptpubkey_address === this.address && this.address !== ''
 | 
			
		||||
              }">
 | 
			
		||||
                <td class="arrow-td">
 | 
			
		||||
@ -146,7 +146,7 @@
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
            <tr *ngIf="tx.vin.length > rowLimit && tx['@vinLimit']">
 | 
			
		||||
            <tr *ngIf="tx.vin.length > inputRowLimit && tx['@vinLimit']">
 | 
			
		||||
              <td colspan="3" class="text-center">
 | 
			
		||||
                <button class="btn btn-sm btn-primary mt-2" (click)="loadMoreInputs(tx);"><span i18n="show-all">Show all</span> ({{ tx.vin.length }})</button>
 | 
			
		||||
              </td>
 | 
			
		||||
@ -158,7 +158,7 @@
 | 
			
		||||
      <div class="col mobile-bottomcol">
 | 
			
		||||
        <table class="table table-borderless smaller-text table-sm table-tx-vout">
 | 
			
		||||
          <tbody>
 | 
			
		||||
            <ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx['@voutLimit'] && !outputIndex ? ((tx.vout.length > rowLimit) ? tx.vout.slice(0, rowLimit - 2) : tx.vout.slice(0, rowLimit)) : tx.vout" [ngForTrackBy]="trackByIndexFn">
 | 
			
		||||
            <ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx['@voutLimit'] ? ((tx.vout.length > outputRowLimit) ? tx.vout.slice(0, outputRowLimit - 2) : tx.vout.slice(0, outputRowLimit)) : tx.vout" [ngForTrackBy]="trackByIndexFn">
 | 
			
		||||
              <tr [ngClass]="{
 | 
			
		||||
                'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
 | 
			
		||||
                'highlight': vout.scriptpubkey_address === this.address && this.address !== ''
 | 
			
		||||
@ -220,7 +220,7 @@
 | 
			
		||||
                      <fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
                    </span>
 | 
			
		||||
                    <ng-template #spent>
 | 
			
		||||
                      <a *ngIf="tx._outspends[vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, tx._outspends[vindex].txid]" class="red">
 | 
			
		||||
                      <a *ngIf="tx._outspends[vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, tx._outspends[vindex].vin + ':' + tx._outspends[vindex].txid]" class="red">
 | 
			
		||||
                        <fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
                      </a>
 | 
			
		||||
                      <ng-template #outputNoTxId>
 | 
			
		||||
@ -257,7 +257,7 @@
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
            <tr *ngIf="tx.vout.length > rowLimit && tx['@voutLimit'] && !outputIndex">
 | 
			
		||||
            <tr *ngIf="tx.vout.length > outputRowLimit && tx['@voutLimit']">
 | 
			
		||||
              <td colspan="3" class="text-center">
 | 
			
		||||
                <button class="btn btn-sm btn-primary mt-2" (click)="tx['@voutLimit'] = false;"><span i18n="show-all">Show all</span> ({{ tx.vout.length }})</button>
 | 
			
		||||
              </td>
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
  @Input() transactionPage = false;
 | 
			
		||||
  @Input() errorUnblinded = false;
 | 
			
		||||
  @Input() paginated = false;
 | 
			
		||||
  @Input() inputIndex: number;
 | 
			
		||||
  @Input() outputIndex: number;
 | 
			
		||||
  @Input() address: string = '';
 | 
			
		||||
  @Input() rowLimit = 12;
 | 
			
		||||
@ -37,6 +38,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
  showDetails$ = new BehaviorSubject<boolean>(false);
 | 
			
		||||
  assetsMinimal: any;
 | 
			
		||||
  transactionsLength: number = 0;
 | 
			
		||||
  inputRowLimit: number = 12;
 | 
			
		||||
  outputRowLimit: number = 12;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
@ -97,50 +100,57 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
    ).subscribe(() => this.ref.markForCheck());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(): void {
 | 
			
		||||
    if (!this.transactions || !this.transactions.length) {
 | 
			
		||||
      return;
 | 
			
		||||
  ngOnChanges(changes): void {
 | 
			
		||||
    if (changes.inputIndex || changes.outputIndex || changes.rowLimit) {
 | 
			
		||||
      this.inputRowLimit = Math.max(this.rowLimit, (this.inputIndex || 0) + 3);
 | 
			
		||||
      this.outputRowLimit = Math.max(this.rowLimit, (this.outputIndex || 0) + 3);
 | 
			
		||||
      if ((this.inputIndex || this.outputIndex) && !changes.transactions) {
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          const assetBoxElements = document.getElementsByClassName('assetBox');
 | 
			
		||||
          if (assetBoxElements && assetBoxElements[0]) {
 | 
			
		||||
            assetBoxElements[0].scrollIntoView({block: "center"});
 | 
			
		||||
          }
 | 
			
		||||
        }, 10);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.transactionsLength = this.transactions.length;
 | 
			
		||||
    if (this.outputIndex) {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        const assetBoxElements = document.getElementsByClassName('assetBox');
 | 
			
		||||
        if (assetBoxElements && assetBoxElements[0]) {
 | 
			
		||||
          assetBoxElements[0].scrollIntoView();
 | 
			
		||||
        }
 | 
			
		||||
      }, 10);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.transactions.forEach((tx) => {
 | 
			
		||||
      tx['@voutLimit'] = true;
 | 
			
		||||
      tx['@vinLimit'] = true;
 | 
			
		||||
      if (tx['addressValue'] !== undefined) {
 | 
			
		||||
    if (changes.transactions || changes.address) {
 | 
			
		||||
      if (!this.transactions || !this.transactions.length) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this.address) {
 | 
			
		||||
        const addressIn = tx.vout
 | 
			
		||||
          .filter((v: Vout) => v.scriptpubkey_address === this.address)
 | 
			
		||||
          .map((v: Vout) => v.value || 0)
 | 
			
		||||
          .reduce((a: number, b: number) => a + b, 0);
 | 
			
		||||
      this.transactionsLength = this.transactions.length;
 | 
			
		||||
 | 
			
		||||
        const addressOut = tx.vin
 | 
			
		||||
          .filter((v: Vin) => v.prevout && v.prevout.scriptpubkey_address === this.address)
 | 
			
		||||
          .map((v: Vin) => v.prevout.value || 0)
 | 
			
		||||
          .reduce((a: number, b: number) => a + b, 0);
 | 
			
		||||
 | 
			
		||||
        tx['addressValue'] = addressIn - addressOut;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    const txIds = this.transactions.filter((tx) => !tx._outspends).map((tx) => tx.txid);
 | 
			
		||||
    if (txIds.length) {
 | 
			
		||||
      this.refreshOutspends$.next(txIds);
 | 
			
		||||
    }
 | 
			
		||||
    if (this.stateService.env.LIGHTNING) {
 | 
			
		||||
      const txIds = this.transactions.filter((tx) => !tx._channels).map((tx) => tx.txid);
 | 
			
		||||
      this.transactions.forEach((tx) => {
 | 
			
		||||
        tx['@voutLimit'] = true;
 | 
			
		||||
        tx['@vinLimit'] = true;
 | 
			
		||||
        if (tx['addressValue'] !== undefined) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.address) {
 | 
			
		||||
          const addressIn = tx.vout
 | 
			
		||||
            .filter((v: Vout) => v.scriptpubkey_address === this.address)
 | 
			
		||||
            .map((v: Vout) => v.value || 0)
 | 
			
		||||
            .reduce((a: number, b: number) => a + b, 0);
 | 
			
		||||
 | 
			
		||||
          const addressOut = tx.vin
 | 
			
		||||
            .filter((v: Vin) => v.prevout && v.prevout.scriptpubkey_address === this.address)
 | 
			
		||||
            .map((v: Vin) => v.prevout.value || 0)
 | 
			
		||||
            .reduce((a: number, b: number) => a + b, 0);
 | 
			
		||||
 | 
			
		||||
          tx['addressValue'] = addressIn - addressOut;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      const txIds = this.transactions.filter((tx) => !tx._outspends).map((tx) => tx.txid);
 | 
			
		||||
      if (txIds.length) {
 | 
			
		||||
        this.refreshChannels$.next(txIds);
 | 
			
		||||
        this.refreshOutspends$.next(txIds);
 | 
			
		||||
      }
 | 
			
		||||
      if (this.stateService.env.LIGHTNING) {
 | 
			
		||||
        const txIds = this.transactions.filter((tx) => !tx._channels).map((tx) => tx.txid);
 | 
			
		||||
        if (txIds.length) {
 | 
			
		||||
          this.refreshChannels$.next(txIds);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -44,7 +44,7 @@
 | 
			
		||||
          <span *ngSwitchCase="'output'" i18n="transaction.output">Output</span>
 | 
			
		||||
          <span *ngSwitchCase="'fee'" i18n="transaction.fee">Fee</span>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
        <span *ngIf="line.type !== 'fee'"> #{{ line.index }}</span>
 | 
			
		||||
        <span *ngIf="line.type !== 'fee'"> #{{ line.index + 1 }}</span>
 | 
			
		||||
      </p>
 | 
			
		||||
      <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>
 | 
			
		||||
 | 
			
		||||
@ -41,6 +41,18 @@
 | 
			
		||||
        <stop offset="98%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
        <stop offset="100%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
      </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]" />
 | 
			
		||||
        <stop offset="30%" stop-color="#1bd8f4" />
 | 
			
		||||
        <stop offset="100%" [attr.stop-color]="gradient[1]" />
 | 
			
		||||
      </linearGradient>
 | 
			
		||||
      <linearGradient id="output-highlight-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
 | 
			
		||||
        <stop offset="0%" [attr.stop-color]="gradient[1]" />
 | 
			
		||||
        <stop offset="70%" stop-color="#1bd8f4" />
 | 
			
		||||
        <stop offset="98%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
        <stop offset="100%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
      </linearGradient>
 | 
			
		||||
      <linearGradient id="fee-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
 | 
			
		||||
        <stop offset="0%" [attr.stop-color]="gradient[1]" />
 | 
			
		||||
        <stop offset="100%" stop-color="white" />
 | 
			
		||||
@ -56,20 +68,24 @@
 | 
			
		||||
      <path
 | 
			
		||||
        [attr.d]="input.path"
 | 
			
		||||
        class="line {{input.class}}"
 | 
			
		||||
        [class.highlight]="inputData[i].index === inputIndex"
 | 
			
		||||
        [style]="input.style"
 | 
			
		||||
        attr.marker-start="url(#{{input.class}}-arrow)"
 | 
			
		||||
        (pointerover)="onHover($event, 'input', i);"
 | 
			
		||||
        (pointerout)="onBlur($event, 'input', i);"
 | 
			
		||||
        (click)="onClick($event, 'input', inputData[i].index);"
 | 
			
		||||
      />
 | 
			
		||||
    </ng-container>
 | 
			
		||||
    <ng-container *ngFor="let output of outputs; let i = index">
 | 
			
		||||
      <path
 | 
			
		||||
        [attr.d]="output.path"
 | 
			
		||||
        class="line {{output.class}}"
 | 
			
		||||
        [class.highlight]="outputData[i].index === outputIndex"
 | 
			
		||||
        [style]="output.style"
 | 
			
		||||
        attr.marker-start="url(#{{output.class}}-arrow)"
 | 
			
		||||
        (pointerover)="onHover($event, 'output', i);"
 | 
			
		||||
        (pointerout)="onBlur($event, 'output', i);"
 | 
			
		||||
        (click)="onClick($event, 'output', outputData[i].index);"
 | 
			
		||||
      />
 | 
			
		||||
    </ng-container>
 | 
			
		||||
  </svg>
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,17 @@
 | 
			
		||||
      stroke: url(#fee-gradient);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.highlight {
 | 
			
		||||
      z-index: 8;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
      &.input {
 | 
			
		||||
        stroke: url(#input-highlight-gradient);
 | 
			
		||||
      }
 | 
			
		||||
      &.output {
 | 
			
		||||
        stroke: url(#output-highlight-gradient);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      z-index: 10;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,11 @@
 | 
			
		||||
import { Component, OnInit, Input, OnChanges, HostListener } from '@angular/core';
 | 
			
		||||
import { Transaction } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, HostListener } from '@angular/core';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { Outspend, Transaction } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
import { ReplaySubject, merge, Subscription } from 'rxjs';
 | 
			
		||||
import { tap, switchMap } from 'rxjs/operators';
 | 
			
		||||
import { ApiService } from '../../services/api.service';
 | 
			
		||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
 | 
			
		||||
 | 
			
		||||
interface SvgLine {
 | 
			
		||||
  path: string;
 | 
			
		||||
@ -34,6 +40,11 @@ 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() inputIndex: number;
 | 
			
		||||
  @Input() outputIndex: number;
 | 
			
		||||
 | 
			
		||||
  @Output() selectInput = new EventEmitter<number>();
 | 
			
		||||
  @Output() selectOutput = new EventEmitter<number>();
 | 
			
		||||
 | 
			
		||||
  inputData: Xput[];
 | 
			
		||||
  outputData: Xput[];
 | 
			
		||||
@ -45,6 +56,10 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
  isLiquid: boolean = false;
 | 
			
		||||
  hoverLine: Xput | void = null;
 | 
			
		||||
  tooltipPosition = { x: 0, y: 0 };
 | 
			
		||||
  outspends: Outspend[] = [];
 | 
			
		||||
 | 
			
		||||
  outspendsSubscription: Subscription;
 | 
			
		||||
  refreshOutspends$: ReplaySubject<string> = new ReplaySubject();
 | 
			
		||||
 | 
			
		||||
  gradientColors = {
 | 
			
		||||
    '': ['#9339f4', '#105fb0'],
 | 
			
		||||
@ -61,12 +76,45 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
 | 
			
		||||
  gradient: string[] = ['#105fb0', '#105fb0'];
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    private relativeUrlPipe: RelativeUrlPipe,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.initGraph();
 | 
			
		||||
 | 
			
		||||
    this.outspendsSubscription = merge(
 | 
			
		||||
      this.refreshOutspends$
 | 
			
		||||
        .pipe(
 | 
			
		||||
          switchMap((txid) => this.apiService.getOutspendsBatched$([txid])),
 | 
			
		||||
          tap((outspends: Outspend[][]) => {
 | 
			
		||||
            if (!this.tx || !outspends || !outspends.length) {
 | 
			
		||||
              return;
 | 
			
		||||
            }
 | 
			
		||||
            this.outspends = outspends[0];
 | 
			
		||||
          }),
 | 
			
		||||
        ),
 | 
			
		||||
      this.stateService.utxoSpent$
 | 
			
		||||
        .pipe(
 | 
			
		||||
          tap((utxoSpent) => {
 | 
			
		||||
            for (const i in utxoSpent) {
 | 
			
		||||
              this.outspends[i] = {
 | 
			
		||||
                spent: true,
 | 
			
		||||
                txid: utxoSpent[i].txid,
 | 
			
		||||
                vin: utxoSpent[i].vin,
 | 
			
		||||
              };
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        ),
 | 
			
		||||
    ).subscribe(() => {});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(): void {
 | 
			
		||||
    this.initGraph();
 | 
			
		||||
    this.refreshOutspends$.next(this.tx.txid);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  initGraph(): void {
 | 
			
		||||
@ -76,11 +124,12 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
    this.combinedWeight = Math.min(this.maxCombinedWeight, Math.floor((this.width - (2 * this.midWidth)) / 6));
 | 
			
		||||
 | 
			
		||||
    const totalValue = this.calcTotalValue(this.tx);
 | 
			
		||||
    let voutWithFee = this.tx.vout.map(v => {
 | 
			
		||||
    let voutWithFee = this.tx.vout.map((v, i) => {
 | 
			
		||||
      return {
 | 
			
		||||
        type: v.scriptpubkey_type === 'fee' ? 'fee' : 'output',
 | 
			
		||||
        value: v?.value,
 | 
			
		||||
        address: v?.scriptpubkey_address || v?.scriptpubkey_type?.toUpperCase(),
 | 
			
		||||
        index: i,
 | 
			
		||||
        pegout: v?.pegout?.scriptpubkey_address,
 | 
			
		||||
        confidential: (this.isLiquid && v?.value === undefined),
 | 
			
		||||
      } as Xput;
 | 
			
		||||
@ -91,11 +140,12 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
    }
 | 
			
		||||
    const outputCount = voutWithFee.length;
 | 
			
		||||
 | 
			
		||||
    let truncatedInputs = this.tx.vin.map(v => {
 | 
			
		||||
    let truncatedInputs = this.tx.vin.map((v, i) => {
 | 
			
		||||
      return {
 | 
			
		||||
        type: 'input',
 | 
			
		||||
        value: v?.prevout?.value,
 | 
			
		||||
        address: v?.prevout?.scriptpubkey_address || v?.prevout?.scriptpubkey_type?.toUpperCase(),
 | 
			
		||||
        index: i,
 | 
			
		||||
        coinbase: v?.is_coinbase,
 | 
			
		||||
        pegin: v?.is_pegin,
 | 
			
		||||
        confidential: (this.isLiquid && v?.prevout?.value === undefined),
 | 
			
		||||
@ -306,8 +356,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
      };
 | 
			
		||||
    } else {
 | 
			
		||||
      this.hoverLine = {
 | 
			
		||||
        ...this.outputData[index],
 | 
			
		||||
        index
 | 
			
		||||
        ...this.outputData[index]
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -315,4 +364,29 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
  onBlur(event, side, index): void {
 | 
			
		||||
    this.hoverLine = null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onClick(event, side, index): void {
 | 
			
		||||
    if (side === 'input') {
 | 
			
		||||
      const input = this.tx.vin[index];
 | 
			
		||||
      if (input && input.txid && input.vout != null) {
 | 
			
		||||
        this.router.navigate([this.relativeUrlPipe.transform('/tx'), input.txid + ':' + input.vout], {
 | 
			
		||||
          queryParamsHandling: 'merge',
 | 
			
		||||
          fragment: 'flow'
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        this.selectInput.emit(index);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      const output = this.tx.vout[index];
 | 
			
		||||
      const outspend = this.outspends[index];
 | 
			
		||||
      if (output && outspend && outspend.spent && outspend.txid) {
 | 
			
		||||
        this.router.navigate([this.relativeUrlPipe.transform('/tx'), outspend.vin + ':' + outspend.txid], {
 | 
			
		||||
          queryParamsHandling: 'merge',
 | 
			
		||||
          fragment: 'flow'
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        this.selectOutput.emit(index);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user