Merge branch 'master' into simon/show-ln-capacity-on-mobile
This commit is contained in:
		
						commit
						b638719e72
					
				| @ -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 { | ||||
| .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; | ||||
|   } | ||||
| } | ||||
| @ -147,3 +147,9 @@ | ||||
|     transform: scaleX(-1); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| :host-context(.rtl-layout) { | ||||
|   #arrow-up { | ||||
|     transform: translateX(70px); | ||||
|   } | ||||
| } | ||||
| @ -287,11 +287,12 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     this.arrowVisible = true; | ||||
| 
 | ||||
|     for (const block of this.mempoolBlocks) { | ||||
|       for (let i = 0; i < block.feeRange.length - 1; i++) { | ||||
|     let found = false; | ||||
|     for (let txInBlockIndex = 0; txInBlockIndex < this.mempoolBlocks.length && !found; txInBlockIndex++) { | ||||
|       const block = this.mempoolBlocks[txInBlockIndex]; | ||||
|       for (let i = 0; i < block.feeRange.length - 1 && !found; i++) { | ||||
|         if (this.txFeePerVSize < block.feeRange[i + 1] && this.txFeePerVSize >= block.feeRange[i]) { | ||||
|           const txInBlockIndex = this.mempoolBlocks.indexOf(block); | ||||
|           const feeRangeIndex = block.feeRange.findIndex((val, index) => this.txFeePerVSize < block.feeRange[index + 1]); | ||||
|           const feeRangeIndex = i; | ||||
|           const feeRangeChunkSize = 1 / (block.feeRange.length - 1); | ||||
| 
 | ||||
|           const txFee = this.txFeePerVSize - block.feeRange[i]; | ||||
| @ -306,9 +307,13 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { | ||||
|             + ((1 - feePosition) * blockedFilledPercentage * this.blockWidth); | ||||
| 
 | ||||
|           this.rightPosition = arrowRightPosition; | ||||
|           break; | ||||
|           found = true; | ||||
|         } | ||||
|       } | ||||
|       if (this.txFeePerVSize >= block.feeRange[block.feeRange.length - 1]) { | ||||
|         this.rightPosition = txInBlockIndex * (this.blockWidth + this.blockPadding); | ||||
|         found = true; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -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,36 +3,36 @@ | ||||
| } | ||||
| 
 | ||||
| .container-buttons { | ||||
|   align-self: center; | ||||
|   align-self: flex-start; | ||||
| } | ||||
| 
 | ||||
| .title-block { | ||||
|   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) { | ||||
|   @media (max-width: 650px) { | ||||
|     order: 3; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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(':'); | ||||
|           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,20 +100,26 @@ export class TransactionsListComponent implements OnInit, OnChanges { | ||||
|     ).subscribe(() => this.ref.markForCheck()); | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges(): void { | ||||
|   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); | ||||
|       } | ||||
|     } | ||||
|     if (changes.transactions || changes.address) { | ||||
|       if (!this.transactions || !this.transactions.length) { | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       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; | ||||
| @ -144,6 +153,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   onScroll(): void { | ||||
|     const scrollHeight = document.body.scrollHeight; | ||||
|  | ||||
| @ -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); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| <div class="widget-toggler"> | ||||
|   <a href="javascript:;" (click)="switchMode('avg')" class="toggler-option" | ||||
|     [ngClass]="{'inactive': mode !== 'avg'}"><small>avg</small></a> | ||||
|   <a href="" (click)="switchMode('avg')" class="toggler-option" | ||||
|     [ngClass]="{'inactive': mode === 'avg'}"><small>avg</small></a> | ||||
|   <span style="color: #ffffff66; font-size: 8px"> | </span> | ||||
|   <a href="javascript:;" (click)="switchMode('med')" class="toggler-option" | ||||
|     [ngClass]="{'inactive': mode !== 'med'}"><small>med</small></a> | ||||
|   <a href="" (click)="switchMode('med')" class="toggler-option" | ||||
|     [ngClass]="{'inactive': mode === 'med'}"><small>med</small></a> | ||||
| </div> | ||||
| 
 | ||||
| <div class="fee-estimation-wrapper" *ngIf="statistics$ | async as statistics; else loadingReward"> | ||||
|  | ||||
| @ -18,5 +18,6 @@ export class ChannelsStatisticsComponent implements OnInit { | ||||
| 
 | ||||
|   switchMode(mode: 'avg' | 'med') { | ||||
|     this.mode = mode; | ||||
|     return false; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; | ||||
| import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core'; | ||||
| import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs'; | ||||
| import { Transaction } from '../interfaces/electrs.interface'; | ||||
| import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface'; | ||||
| @ -113,6 +113,7 @@ export class StateService { | ||||
| 
 | ||||
|   constructor( | ||||
|     @Inject(PLATFORM_ID) private platformId: any, | ||||
|     @Inject(LOCALE_ID) private locale: string, | ||||
|     private router: Router, | ||||
|     private storageService: StorageService, | ||||
|   ) { | ||||
| @ -151,7 +152,10 @@ export class StateService { | ||||
| 
 | ||||
|     this.blockVSize = this.env.BLOCK_WEIGHT_UNITS / 4; | ||||
| 
 | ||||
|     this.timeLtr = new BehaviorSubject<boolean>(this.storageService.getValue('time-preference-ltr') === 'true'); | ||||
|     const savedTimePreference = this.storageService.getValue('time-preference-ltr'); | ||||
|     const rtlLanguage = (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')); | ||||
|     // default time direction is right-to-left, unless locale is a RTL language
 | ||||
|     this.timeLtr = new BehaviorSubject<boolean>(savedTimePreference === 'true' || (savedTimePreference == null && rtlLanguage)); | ||||
|     this.timeLtr.subscribe((ltr) => { | ||||
|       this.storageService.setValue('time-preference-ltr', ltr ? 'true' : 'false'); | ||||
|     }); | ||||
|  | ||||
| @ -112,14 +112,14 @@ class Server { | ||||
|         const screenshot = await page.screenshot(); | ||||
|         return screenshot; | ||||
|       } else if (success === false) { | ||||
|         logger.warn(`failed to render page preview for ${action} due to client-side error, e.g. requested an invalid txid`); | ||||
|         logger.warn(`failed to render ${path} for ${action} due to client-side error, e.g. requested an invalid txid`); | ||||
|         page.repairRequested = true; | ||||
|       } else { | ||||
|         logger.warn(`failed to render page preview for ${action} due to puppeteer timeout`); | ||||
|         logger.warn(`failed to render ${path} for ${action} due to puppeteer timeout`); | ||||
|         page.repairRequested = true; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       logger.err(`failed to render page for ${action}: ` + (e instanceof Error ? e.message : `${e}`)); | ||||
|       logger.err(`failed to render ${path} for ${action}: ` + (e instanceof Error ? e.message : `${e}`)); | ||||
|       page.repairRequested = true; | ||||
|     } | ||||
|   } | ||||
| @ -154,7 +154,7 @@ class Server { | ||||
|         res.send(img); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       logger.err(e instanceof Error ? e.message : `${e}`); | ||||
|       logger.err(e instanceof Error ? e.message : `${e} ${req.params[0]}`); | ||||
|       res.status(500).send(e instanceof Error ? e.message : e); | ||||
|     } | ||||
|   } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user