Merge branch 'master' into simon/mempool-node-group-page
This commit is contained in:
		
						commit
						1b2e7090c3
					
				| @ -1,9 +1,16 @@ | ||||
| <a *ngIf="channel; else default" [routerLink]="['/lightning/channel' | relativeUrl, channel.id]"> | ||||
|   <span | ||||
|     *ngIf="label" | ||||
|     class="badge badge-pill badge-warning" | ||||
|   >{{ label }}</span> | ||||
| </a> | ||||
| <ng-template [ngIf]="channel" [ngIfElse]="default"> | ||||
|   <div> | ||||
|     <div class="badge-positioner"> | ||||
|       <a [routerLink]="['/lightning/channel' | relativeUrl, channel.id]"> | ||||
|         <span  | ||||
|           *ngIf="label" | ||||
|           class="badge badge-pill badge-warning" | ||||
|         >{{ label }}</span> | ||||
|       </a> | ||||
|     </div> | ||||
|       | ||||
|   </div> | ||||
| </ng-template> | ||||
| 
 | ||||
| <ng-template #default> | ||||
|   <span | ||||
|  | ||||
| @ -1,3 +1,7 @@ | ||||
| .badge { | ||||
|   margin-right: 2px; | ||||
| } | ||||
| 
 | ||||
| .badge-positioner { | ||||
|   position: absolute; | ||||
| } | ||||
							
								
								
									
										34
									
								
								frontend/src/app/components/pool/pool-preview.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								frontend/src/app/components/pool/pool-preview.component.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | ||||
| <div class="box preview-box" *ngIf="poolStats$ | async as poolStats"> | ||||
|   <app-preview-title> | ||||
|     <span i18n="mining.pools">mining pool</span> | ||||
|   </app-preview-title> | ||||
|   <div class="row d-flex justify-content-between full-width-row"> | ||||
|     <div class="title-wrapper"> | ||||
|       <h1 class="title">{{ poolStats.pool.name }}</h1> | ||||
|     </div> | ||||
|     <div class="logo-wrapper"> | ||||
|       <img width="62" height="62" src="/resources/mining-pools/default.svg"> | ||||
|       <img [class.noimg]="!imageLoaded" width="62" height="62" src="{{ poolStats['logo'] }}" | ||||
|         (load)="onImageLoad()" (error)="onImageFail()"> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="row full-width-row"> | ||||
|       <div class="stats"> | ||||
|         <div class="stat-box"> | ||||
|           <div class="label" i18n="mining.tags">Tags</div> | ||||
|           <div *ngIf="poolStats.pool.regexes.length else nodata" class="data">{{ poolStats.pool.regexes }}</div> | ||||
|         </div> | ||||
|         <div class="stat-box"> | ||||
|           <div class="label" i18n="mining.hashrate">Hashrate</div> | ||||
|           <div class="data">{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</div> | ||||
|         </div> | ||||
|       </div> | ||||
|   </div> | ||||
|   <div class="row hash-chart full-width-row"> | ||||
|     <div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartFinished)="onChartReady()"></div> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
| <ng-template #nodata> | ||||
|   <div>~</div> | ||||
| </ng-template> | ||||
							
								
								
									
										78
									
								
								frontend/src/app/components/pool/pool-preview.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								frontend/src/app/components/pool/pool-preview.component.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,78 @@ | ||||
| .stats { | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   justify-content: center; | ||||
|   align-items: flex-start; | ||||
|   width: 100%; | ||||
|   max-width: 100%; | ||||
|   margin: 15px 0; | ||||
|   font-size: 32px; | ||||
|   overflow: hidden; | ||||
| 
 | ||||
|   .stat-box { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     flex-wrap: nowrap; | ||||
|     align-items: baseline; | ||||
|     justify-content: space-between; | ||||
|     width: 100%; | ||||
|     margin-left: 15px; | ||||
|     background: #181b2d; | ||||
|     padding: 0.75rem; | ||||
|     width: 0; | ||||
|     flex-grow: 1; | ||||
| 
 | ||||
|     &:first-child { | ||||
|       margin-left: 0; | ||||
|     } | ||||
| 
 | ||||
|     .label { | ||||
|       flex-shrink: 0; | ||||
|       flex-grow: 0; | ||||
|       margin-right: 1em; | ||||
|     } | ||||
|     .data { | ||||
|       flex-shrink: 1; | ||||
|       overflow: hidden; | ||||
|       text-overflow: ellipsis; | ||||
|       white-space: nowrap; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .chart { | ||||
|   width: 100%; | ||||
|   height: 315px; | ||||
|   background: #181b2d; | ||||
| } | ||||
| 
 | ||||
| .row { | ||||
|   margin-right: 0; | ||||
| } | ||||
| 
 | ||||
| .full-width-row { | ||||
|   padding-left: 15px; | ||||
|   flex-wrap: nowrap; | ||||
| } | ||||
| 
 | ||||
| .logo-wrapper { | ||||
|   position: relative; | ||||
|   width: 62px; | ||||
|   height: 62px; | ||||
|   margin-left: 1em; | ||||
| 
 | ||||
|   img { | ||||
|     position: absolute; | ||||
|     right: 0; | ||||
|     top: 0; | ||||
|     background: #24273e; | ||||
| 
 | ||||
|     &.noimg { | ||||
|       opacity: 0; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| ::ng-deep .symbol { | ||||
|   font-size: 24px; | ||||
| } | ||||
							
								
								
									
										187
									
								
								frontend/src/app/components/pool/pool-preview.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								frontend/src/app/components/pool/pool-preview.component.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,187 @@ | ||||
| import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core'; | ||||
| import { ActivatedRoute } from '@angular/router'; | ||||
| import { EChartsOption, graphic } from 'echarts'; | ||||
| import { Observable, of } from 'rxjs'; | ||||
| import { map, switchMap, catchError } from 'rxjs/operators'; | ||||
| import { PoolStat } from 'src/app/interfaces/node-api.interface'; | ||||
| import { ApiService } from 'src/app/services/api.service'; | ||||
| import { StateService } from 'src/app/services/state.service'; | ||||
| import { formatNumber } from '@angular/common'; | ||||
| import { SeoService } from 'src/app/services/seo.service'; | ||||
| import { OpenGraphService } from 'src/app/services/opengraph.service'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-pool-preview', | ||||
|   templateUrl: './pool-preview.component.html', | ||||
|   styleUrls: ['./pool-preview.component.scss'], | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush | ||||
| }) | ||||
| export class PoolPreviewComponent implements OnInit { | ||||
|   formatNumber = formatNumber; | ||||
|   poolStats$: Observable<PoolStat>; | ||||
|   isLoading = true; | ||||
|   imageLoaded = false; | ||||
|   lastImgSrc: string = ''; | ||||
| 
 | ||||
|   chartOptions: EChartsOption = {}; | ||||
|   chartInitOptions = { | ||||
|     renderer: 'svg', | ||||
|   }; | ||||
| 
 | ||||
|   slug: string = undefined; | ||||
| 
 | ||||
|   constructor( | ||||
|     @Inject(LOCALE_ID) public locale: string, | ||||
|     private apiService: ApiService, | ||||
|     private route: ActivatedRoute, | ||||
|     public stateService: StateService, | ||||
|     private seoService: SeoService, | ||||
|     private openGraphService: OpenGraphService, | ||||
|   ) { | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.poolStats$ = this.route.params.pipe(map((params) => params.slug)) | ||||
|       .pipe( | ||||
|         switchMap((slug: any) => { | ||||
|           this.isLoading = true; | ||||
|           this.imageLoaded = false; | ||||
|           this.slug = slug; | ||||
|           this.openGraphService.waitFor('pool-hash-' + this.slug); | ||||
|           this.openGraphService.waitFor('pool-stats-' + this.slug); | ||||
|           this.openGraphService.waitFor('pool-chart-' + this.slug); | ||||
|           this.openGraphService.waitFor('pool-img-' + this.slug); | ||||
|           return this.apiService.getPoolHashrate$(this.slug) | ||||
|             .pipe( | ||||
|               switchMap((data) => { | ||||
|                 this.isLoading = false; | ||||
|                 this.prepareChartOptions(data.map(val => [val.timestamp * 1000, val.avgHashrate])); | ||||
|                 this.openGraphService.waitOver('pool-hash-' + this.slug); | ||||
|                 return [slug]; | ||||
|               }), | ||||
|               catchError(() => { | ||||
|                 this.isLoading = false; | ||||
|                 this.openGraphService.fail('pool-hash-' + this.slug); | ||||
|                 return of([slug]); | ||||
|               }) | ||||
|             ); | ||||
|         }), | ||||
|         switchMap((slug) => { | ||||
|           return this.apiService.getPoolStats$(slug).pipe( | ||||
|             catchError(() => { | ||||
|               this.isLoading = false; | ||||
|               this.openGraphService.fail('pool-stats-' + this.slug); | ||||
|               return of(null); | ||||
|             }) | ||||
|           ); | ||||
|         }), | ||||
|         map((poolStats) => { | ||||
|           if (poolStats == null) { | ||||
|             return null; | ||||
|           } | ||||
| 
 | ||||
|           this.seoService.setTitle(poolStats.pool.name); | ||||
|           let regexes = '"'; | ||||
|           for (const regex of poolStats.pool.regexes) { | ||||
|             regexes += regex + '", "'; | ||||
|           } | ||||
|           poolStats.pool.regexes = regexes.slice(0, -3); | ||||
|           poolStats.pool.addresses = poolStats.pool.addresses; | ||||
| 
 | ||||
|           if (poolStats.reportedHashrate) { | ||||
|             poolStats.luck = poolStats.estimatedHashrate / poolStats.reportedHashrate * 100; | ||||
|           } | ||||
| 
 | ||||
|           this.openGraphService.waitOver('pool-stats-' + this.slug); | ||||
| 
 | ||||
|           const logoSrc = `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'; | ||||
|           if (logoSrc === this.lastImgSrc) { | ||||
|             this.openGraphService.waitOver('pool-img-' + this.slug); | ||||
|           } | ||||
|           this.lastImgSrc = logoSrc; | ||||
|           return Object.assign({ | ||||
|             logo: logoSrc | ||||
|           }, poolStats); | ||||
|         }), | ||||
|         catchError(() => { | ||||
|           this.isLoading = false; | ||||
|           this.openGraphService.fail('pool-stats-' + this.slug); | ||||
|           return of(null); | ||||
|         }) | ||||
|       ); | ||||
|   } | ||||
| 
 | ||||
|   prepareChartOptions(data) { | ||||
|     let title: object; | ||||
|     if (data.length === 0) { | ||||
|       title = { | ||||
|         textStyle: { | ||||
|           color: 'grey', | ||||
|           fontSize: 15 | ||||
|         }, | ||||
|         text: $localize`:@@23555386d8af1ff73f297e89dd4af3f4689fb9dd:Indexing blocks`, | ||||
|         left: 'center', | ||||
|         top: 'center' | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
|     this.chartOptions = { | ||||
|       title: title, | ||||
|       animation: false, | ||||
|       color: [ | ||||
|         new graphic.LinearGradient(0, 0, 0, 0.65, [ | ||||
|           { offset: 0, color: '#F4511E' }, | ||||
|           { offset: 0.25, color: '#FB8C00' }, | ||||
|           { offset: 0.5, color: '#FFB300' }, | ||||
|           { offset: 0.75, color: '#FDD835' }, | ||||
|           { offset: 1, color: '#7CB342' } | ||||
|         ]), | ||||
|         '#D81B60', | ||||
|       ], | ||||
|       grid: { | ||||
|         left: 15, | ||||
|         right: 15, | ||||
|         bottom: 15, | ||||
|         top: 15, | ||||
|         show: false, | ||||
|       }, | ||||
|       xAxis: data.length === 0 ? undefined : { | ||||
|         type: 'time', | ||||
|         show: false, | ||||
|       }, | ||||
|       yAxis: data.length === 0 ? undefined : [ | ||||
|         { | ||||
|           type: 'value', | ||||
|           show: false, | ||||
|         }, | ||||
|       ], | ||||
|       series: data.length === 0 ? undefined : [ | ||||
|         { | ||||
|           zlevel: 0, | ||||
|           name: 'Hashrate', | ||||
|           showSymbol: false, | ||||
|           symbol: 'none', | ||||
|           data: data, | ||||
|           type: 'line', | ||||
|           lineStyle: { | ||||
|             width: 4, | ||||
|           }, | ||||
|         }, | ||||
|       ], | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   onChartReady(): void { | ||||
|     this.openGraphService.waitOver('pool-chart-' + this.slug); | ||||
|   } | ||||
| 
 | ||||
|   onImageLoad(): void { | ||||
|     this.imageLoaded = true; | ||||
|     this.openGraphService.waitOver('pool-img-' + this.slug); | ||||
|   } | ||||
| 
 | ||||
|   onImageFail(): void { | ||||
|     this.imageLoaded = false; | ||||
|     this.openGraphService.waitOver('pool-img-' + this.slug); | ||||
|   } | ||||
| } | ||||
| @ -190,6 +190,24 @@ | ||||
| 
 | ||||
|     <br> | ||||
| 
 | ||||
|     <div class="title"> | ||||
|       <h2 i18n="transaction.diagram|Transaction diagram">Diagram</h2> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="box"> | ||||
|       <div class="graph-container" #graphContainer> | ||||
|         <tx-bowtie-graph [tx]="tx" [width]="graphWidth" [height]="graphExpanded ? (maxInOut * 15) : graphHeight" [maxStrands]="graphExpanded ? maxInOut : 24" [network]="network" [tooltip]="true"></tx-bowtie-graph> | ||||
|       </div> | ||||
|       <div class="toggle-wrapper" *ngIf="maxInOut > 24"> | ||||
|         <button class="btn btn-sm btn-primary graph-toggle" (click)="expandGraph();" *ngIf="!graphExpanded; else collapseBtn"><span i18n="show-more">Show more</span></button> | ||||
|         <ng-template #collapseBtn> | ||||
|           <button class="btn btn-sm btn-primary graph-toggle" (click)="collapseGraph();"><span i18n="show-less">Show less</span></button> | ||||
|         </ng-template> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <br> | ||||
| 
 | ||||
|     <div class="title float-left"> | ||||
|       <h2 i18n="transaction.inputs-and-outputs|Transaction inputs and outputs">Inputs & Outputs</h2> | ||||
|     </div> | ||||
| @ -283,6 +301,36 @@ | ||||
| 
 | ||||
|     <br> | ||||
| 
 | ||||
|     <div class="title"> | ||||
|       <h2 i18n="transaction.diagram|Transaction diagram">Diagram</h2> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="box"> | ||||
|       <div class="graph-container" #graphContainer style="visibility: hidden;"></div> | ||||
|       <div class="row"> | ||||
|         <div class="col-sm"> | ||||
|           <table class="table table-borderless table-striped"> | ||||
|             <tbody> | ||||
|               <tr> | ||||
|                 <td><span class="skeleton-loader"></span></td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </div> | ||||
|         <div class="col-sm"> | ||||
|           <table class="table table-borderless table-striped"> | ||||
|             <tbody> | ||||
|               <tr> | ||||
|                 <td><span class="skeleton-loader"></span></td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <br> | ||||
| 
 | ||||
|     <div class="title"> | ||||
|       <h2 i18n="transaction.inputs-and-outputs|Transaction inputs and outputs">Inputs & Outputs</h2> | ||||
|     </div> | ||||
|  | ||||
| @ -73,6 +73,24 @@ | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .graph-container { | ||||
|   position: relative; | ||||
|   width: 100%; | ||||
|   background: #181b2d; | ||||
|   padding: 10px; | ||||
|   padding-bottom: 0; | ||||
| } | ||||
| 
 | ||||
| .toggle-wrapper { | ||||
|   width: 100%; | ||||
|   text-align: center; | ||||
|   margin: 1.25em 0 0; | ||||
| } | ||||
| 
 | ||||
| .graph-toggle { | ||||
|   margin: auto; | ||||
| } | ||||
| 
 | ||||
| @media (max-width: 767.98px) { | ||||
| 	.mobile-bottomcol { | ||||
| 		margin-top: 15px; | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { Component, OnInit, OnDestroy } from '@angular/core'; | ||||
| import { Component, OnInit, AfterViewInit, OnDestroy, HostListener, ViewChild, ElementRef } from '@angular/core'; | ||||
| import { ElectrsApiService } from '../../services/electrs-api.service'; | ||||
| import { ActivatedRoute, ParamMap } from '@angular/router'; | ||||
| import { | ||||
| @ -24,7 +24,7 @@ import { LiquidUnblinding } from './liquid-ublinding'; | ||||
|   templateUrl: './transaction.component.html', | ||||
|   styleUrls: ['./transaction.component.scss'], | ||||
| }) | ||||
| export class TransactionComponent implements OnInit, OnDestroy { | ||||
| export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|   network = ''; | ||||
|   tx: Transaction; | ||||
|   txId: string; | ||||
| @ -47,6 +47,14 @@ export class TransactionComponent implements OnInit, OnDestroy { | ||||
|   timeAvg$: Observable<number>; | ||||
|   liquidUnblinding = new LiquidUnblinding(); | ||||
|   outputIndex: number; | ||||
|   graphExpanded: boolean = false; | ||||
|   graphWidth: number = 1000; | ||||
|   graphHeight: number = 360; | ||||
|   maxInOut: number = 0; | ||||
|   tooltipPosition: { x: number, y: number }; | ||||
| 
 | ||||
|   @ViewChild('graphContainer') | ||||
|   graphContainer: ElementRef; | ||||
| 
 | ||||
|   constructor( | ||||
|     private route: ActivatedRoute, | ||||
| @ -167,6 +175,7 @@ export class TransactionComponent implements OnInit, OnDestroy { | ||||
|           this.waitingForTransaction = false; | ||||
|           this.setMempoolBlocksSubscription(); | ||||
|           this.websocketService.startTrackTransaction(tx.txid); | ||||
|           this.setupGraph(); | ||||
| 
 | ||||
|           if (!tx.status.confirmed && tx.firstSeen) { | ||||
|             this.transactionTime = tx.firstSeen; | ||||
| @ -222,6 +231,10 @@ export class TransactionComponent implements OnInit, OnDestroy { | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   ngAfterViewInit(): void { | ||||
|     this.setGraphSize(); | ||||
|   } | ||||
| 
 | ||||
|   handleLoadElectrsTransactionError(error: any): Observable<any> { | ||||
|     if (error.status === 404 && /^[a-fA-F0-9]{64}$/.test(this.txId)) { | ||||
|       this.websocketService.startMultiTrackTransaction(this.txId); | ||||
| @ -284,6 +297,26 @@ export class TransactionComponent implements OnInit, OnDestroy { | ||||
|     return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1); | ||||
|   } | ||||
| 
 | ||||
|   setupGraph() { | ||||
|     this.maxInOut = Math.min(250, Math.max(this.tx?.vin?.length || 1, this.tx?.vout?.length + 1 || 1)); | ||||
|     this.graphHeight = Math.min(360, this.maxInOut * 80); | ||||
|   } | ||||
| 
 | ||||
|   expandGraph() { | ||||
|     this.graphExpanded = true; | ||||
|   } | ||||
| 
 | ||||
|   collapseGraph() { | ||||
|     this.graphExpanded = false; | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('window:resize', ['$event']) | ||||
|   setGraphSize(): void { | ||||
|     if (this.graphContainer) { | ||||
|       this.graphWidth = this.graphContainer.nativeElement.clientWidth - 24; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy() { | ||||
|     this.subscription.unsubscribe(); | ||||
|     this.fetchCpfpSubscription.unsubscribe(); | ||||
|  | ||||
| @ -0,0 +1,56 @@ | ||||
| <div | ||||
|   #tooltip | ||||
|   *ngIf="line" | ||||
|   class="bowtie-graph-tooltip" | ||||
|   [style.visibility]="line ? 'visible' : 'hidden'" | ||||
|   [style.left]="tooltipPosition.x + 'px'" | ||||
|   [style.top]="tooltipPosition.y + 'px'" | ||||
| > | ||||
|   <ng-container *ngIf="line.rest; else coinbase"> | ||||
|     <span>{{ line.rest }} </span> | ||||
|     <ng-container [ngSwitch]="line.type"> | ||||
|       <span *ngSwitchCase="'input'" i18n="transaction.other-inputs">other inputs</span> | ||||
|       <span *ngSwitchCase="'output'" i18n="transaction.other-outputs">other outputs</span> | ||||
|     </ng-container> | ||||
|   </ng-container> | ||||
| 
 | ||||
|   <ng-template #coinbase> | ||||
|     <ng-container *ngIf="line.coinbase; else pegin"> | ||||
|       <p>Coinbase</p> | ||||
|     </ng-container> | ||||
|   </ng-template> | ||||
| 
 | ||||
|   <ng-template #pegin> | ||||
|     <ng-container *ngIf="line.pegin; else pegout"> | ||||
|       <p>Peg In</p> | ||||
|     </ng-container> | ||||
|   </ng-template> | ||||
| 
 | ||||
|   <ng-template #pegout> | ||||
|     <ng-container *ngIf="line.pegout; else normal"> | ||||
|       <p>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> | ||||
|         <span class="last-four">{{ line.pegout.slice(-4) }}</span> | ||||
|       </p> | ||||
|     </ng-container> | ||||
|   </ng-template> | ||||
| 
 | ||||
|   <ng-template #normal> | ||||
|       <p> | ||||
|         <ng-container [ngSwitch]="line.type"> | ||||
|           <span *ngSwitchCase="'input'" i18n="transaction.input">Input</span> | ||||
|           <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> | ||||
|       </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> | ||||
|       <p *ngIf="line.type !== 'fee' && line.address" class="address"> | ||||
|         <span class="first">{{ line.address.slice(0, -4) }}</span> | ||||
|         <span class="last-four">{{ line.address.slice(-4) }}</span> | ||||
|       </p> | ||||
|   </ng-template> | ||||
| </div> | ||||
| @ -0,0 +1,38 @@ | ||||
| .bowtie-graph-tooltip { | ||||
|   position: absolute; | ||||
|   background: rgba(#11131f, 0.95); | ||||
|   border-radius: 4px; | ||||
|   box-shadow: 1px 1px 10px rgba(0,0,0,0.5); | ||||
|   color: #b1b1b1; | ||||
|   padding: 10px 15px; | ||||
|   text-align: left; | ||||
|   pointer-events: none; | ||||
|   max-width: 300px; | ||||
| 
 | ||||
|   p { | ||||
|     margin: 0; | ||||
|     white-space: nowrap; | ||||
|   } | ||||
| 
 | ||||
|   .address { | ||||
|     width: 100%; | ||||
|     max-width: 100%; | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     align-items: baseline; | ||||
|     justify-content: flex-start; | ||||
| 
 | ||||
|     .first { | ||||
|       flex-grow: 0; | ||||
|       flex-shrink: 1; | ||||
|       overflow: hidden; | ||||
|       text-overflow: ellipsis; | ||||
|       margin-right: -2px; | ||||
|     } | ||||
| 
 | ||||
|     .last-four { | ||||
|       flex-shrink: 0; | ||||
|       flex-grow: 0; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,48 @@ | ||||
| import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStrategy } from '@angular/core'; | ||||
| import { TransactionStripped } from 'src/app/interfaces/websocket.interface'; | ||||
| 
 | ||||
| interface Xput { | ||||
|   type: 'input' | 'output' | 'fee'; | ||||
|   value?: number; | ||||
|   index?: number; | ||||
|   address?: string; | ||||
|   rest?: number; | ||||
|   coinbase?: boolean; | ||||
|   pegin?: boolean; | ||||
|   pegout?: string; | ||||
|   confidential?: boolean; | ||||
| } | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-tx-bowtie-graph-tooltip', | ||||
|   templateUrl: './tx-bowtie-graph-tooltip.component.html', | ||||
|   styleUrls: ['./tx-bowtie-graph-tooltip.component.scss'], | ||||
| }) | ||||
| export class TxBowtieGraphTooltipComponent implements OnChanges { | ||||
|   @Input() line: Xput | void; | ||||
|   @Input() cursorPosition: { x: number, y: number }; | ||||
| 
 | ||||
|   tooltipPosition = { x: 0, y: 0 }; | ||||
| 
 | ||||
|   @ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>; | ||||
| 
 | ||||
|   constructor() {} | ||||
| 
 | ||||
|   ngOnChanges(changes): void { | ||||
|     if (changes.cursorPosition && changes.cursorPosition.currentValue) { | ||||
|       let x = Math.max(10, changes.cursorPosition.currentValue.x - 50); | ||||
|       let y = changes.cursorPosition.currentValue.y + 20; | ||||
|       if (this.tooltipElement) { | ||||
|         const elementBounds = this.tooltipElement.nativeElement.getBoundingClientRect(); | ||||
|         const parentBounds = this.tooltipElement.nativeElement.offsetParent.getBoundingClientRect(); | ||||
|         if ((parentBounds.left + x + elementBounds.width) > parentBounds.right) { | ||||
|           x = Math.max(0, parentBounds.width - elementBounds.width - 10); | ||||
|         } | ||||
|         if (y + elementBounds.height > parentBounds.height) { | ||||
|           y = y - elementBounds.height - 20; | ||||
|         } | ||||
|       } | ||||
|       this.tooltipPosition = { x, y }; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,44 +1,82 @@ | ||||
| <svg *ngIf="inputs && outputs" class="bowtie" [attr.height]="(height + 10) + 'px'" [attr.width]="width + 'px'"> | ||||
|   <defs> | ||||
|     <marker id="input-arrow" viewBox="-5 -5 10 10" | ||||
|         refX="0" refY="0" | ||||
|         markerUnits="strokeWidth" | ||||
|         markerWidth="1.5" markerHeight="1" | ||||
|         orient="auto"> | ||||
|       <path d="M -5 -5 L 0 0 L -5 5 L 1 5 L 1 -5 Z" stroke-width="0" [attr.fill]="gradient[0]"/> | ||||
|     </marker> | ||||
|     <marker id="output-arrow" viewBox="-5 -5 10 10" | ||||
|         refX="0" refY="0" | ||||
|         markerUnits="strokeWidth" | ||||
|         markerWidth="1.5" markerHeight="1" | ||||
|         orient="auto"> | ||||
|       <path d="M 1 -5 L 0 -5 L -5 0 L 0 5 L 1 5 Z" stroke-width="0" [attr.fill]="gradient[0]"/> | ||||
|     </marker> | ||||
|     <marker id="fee-arrow" viewBox="-5 -5 10 10" | ||||
|         refX="0" refY="0" | ||||
|         markerUnits="strokeWidth" | ||||
|         markerWidth="1.5" markerHeight="1" | ||||
|         orient="auto"> | ||||
|     </marker> | ||||
|     <linearGradient id="input-gradient" x1="0%" y1="0%" x2="100%" y2="0%"> | ||||
| <div class="bowtie-graph"> | ||||
|   <svg *ngIf="inputs && outputs" class="bowtie" [attr.height]="(height + 10) + 'px'" [attr.width]="width + 'px'"> | ||||
|     <defs> | ||||
|       <marker id="input-arrow" viewBox="-5 -5 10 10" | ||||
|           refX="0" refY="0" | ||||
|           markerUnits="strokeWidth" | ||||
|           markerWidth="1.5" markerHeight="1" | ||||
|           orient="auto"> | ||||
|         <path d="M -5 -5 L 0 0 L -5 5 L 1 5 L 1 -5 Z" stroke-width="0" [attr.fill]="gradient[0]"/> | ||||
|       </marker> | ||||
|       <marker id="output-arrow" viewBox="-5 -5 10 10" | ||||
|           refX="0" refY="0" | ||||
|           markerUnits="strokeWidth" | ||||
|           markerWidth="1.5" markerHeight="1" | ||||
|           orient="auto"> | ||||
|         <path d="M 1 -5 L 0 -5 L -5 0 L 0 5 L 1 5 Z" stroke-width="0" [attr.fill]="gradient[0]"/> | ||||
|       </marker> | ||||
|       <marker id="fee-arrow" viewBox="-5 -5 10 10" | ||||
|           refX="0" refY="0" | ||||
|           markerUnits="strokeWidth" | ||||
|           markerWidth="1.5" markerHeight="1" | ||||
|           orient="auto"> | ||||
|       </marker> | ||||
|       <linearGradient id="input-gradient" x1="0%" y1="0%" x2="100%" y2="0%"> | ||||
|         <stop offset="0%" [attr.stop-color]="gradient[0]" /> | ||||
|         <stop offset="100%" [attr.stop-color]="gradient[1]" /> | ||||
|       </linearGradient> | ||||
|       <linearGradient id="output-gradient" x1="0%" y1="0%" x2="100%" y2="0%"> | ||||
|         <stop offset="0%" [attr.stop-color]="gradient[1]" /> | ||||
|         <stop offset="100%" [attr.stop-color]="gradient[0]" /> | ||||
|       </linearGradient> | ||||
|       <linearGradient id="input-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%"> | ||||
|       <stop offset="0%" [attr.stop-color]="gradient[0]" /> | ||||
|       <stop offset="100%" [attr.stop-color]="gradient[1]" /> | ||||
|     </linearGradient> | ||||
|     <linearGradient id="output-gradient" x1="0%" y1="0%" x2="100%" y2="0%"> | ||||
|       <stop offset="0%" [attr.stop-color]="gradient[1]" /> | ||||
|       <stop offset="100%" [attr.stop-color]="gradient[0]" /> | ||||
|     </linearGradient> | ||||
|     <linearGradient id="fee-gradient" x1="0%" y1="0%" x2="100%" y2="0%"> | ||||
|       <stop offset="0%" [attr.stop-color]="gradient[1]" /> | ||||
|       <stop offset="50%" [attr.stop-color]="gradient[1]" /> | ||||
|       <stop offset="100%" stop-color="transparent" /> | ||||
|     </linearGradient> | ||||
|   </defs> | ||||
|   <path [attr.d]="middle.path" class="line middle" [style]="middle.style"/> | ||||
|   <ng-container *ngFor="let input of inputs"> | ||||
|     <path [attr.d]="input.path" class="line {{input.class}}" [style]="input.style" attr.marker-start="url(#{{input.class}}-arrow)"/> | ||||
|   </ng-container> | ||||
|   <ng-container *ngFor="let output of outputs"> | ||||
|     <path [attr.d]="output.path" class="line {{output.class}}" [style]="output.style" attr.marker-start="url(#{{output.class}}-arrow)" /> | ||||
|   </ng-container> | ||||
| </svg> | ||||
|       <stop offset="2%" [attr.stop-color]="gradient[0]" /> | ||||
|         <stop offset="30%" stop-color="white" /> | ||||
|         <stop offset="100%" [attr.stop-color]="gradient[1]" /> | ||||
|       </linearGradient> | ||||
|       <linearGradient id="output-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%"> | ||||
|         <stop offset="0%" [attr.stop-color]="gradient[1]" /> | ||||
|         <stop offset="70%" stop-color="white" /> | ||||
|         <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" /> | ||||
|       </linearGradient> | ||||
|       <linearGradient id="fee-gradient" x1="0%" y1="0%" x2="100%" y2="0%"> | ||||
|         <stop offset="0%" [attr.stop-color]="gradient[1]" /> | ||||
|         <stop offset="50%" [attr.stop-color]="gradient[1]" /> | ||||
|         <stop offset="100%" stop-color="transparent" /> | ||||
|       </linearGradient> | ||||
|     </defs> | ||||
|     <path [attr.d]="middle.path" class="line middle" [style]="middle.style"/> | ||||
|     <ng-container *ngFor="let input of inputs; let i = index"> | ||||
|       <path | ||||
|         [attr.d]="input.path" | ||||
|         class="line {{input.class}}" | ||||
|         [style]="input.style" | ||||
|         attr.marker-start="url(#{{input.class}}-arrow)" | ||||
|         (pointerover)="onHover($event, 'input', i);" | ||||
|         (pointerout)="onBlur($event, 'input', i);" | ||||
|       /> | ||||
|     </ng-container> | ||||
|     <ng-container *ngFor="let output of outputs; let i = index"> | ||||
|       <path | ||||
|         [attr.d]="output.path" | ||||
|         class="line {{output.class}}" | ||||
|         [style]="output.style" | ||||
|         attr.marker-start="url(#{{output.class}}-arrow)" | ||||
|         (pointerover)="onHover($event, 'output', i);" | ||||
|         (pointerout)="onBlur($event, 'output', i);" | ||||
|       /> | ||||
|     </ng-container> | ||||
|   </svg> | ||||
| 
 | ||||
|   <app-tx-bowtie-graph-tooltip | ||||
|     *ngIf=[tooltip] | ||||
|     [line]="hoverLine" | ||||
|     [cursorPosition]="tooltipPosition" | ||||
|   ></app-tx-bowtie-graph-tooltip> | ||||
| </div> | ||||
|  | ||||
| Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 3.5 KiB | 
| @ -11,5 +11,19 @@ | ||||
|     &.fee { | ||||
|       stroke: url(#fee-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); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { Component, OnInit, Input, OnChanges } from '@angular/core'; | ||||
| import { Component, OnInit, Input, OnChanges, HostListener } from '@angular/core'; | ||||
| import { Transaction } from '../../interfaces/electrs.interface'; | ||||
| 
 | ||||
| interface SvgLine { | ||||
| @ -7,6 +7,20 @@ interface SvgLine { | ||||
|   class?: string; | ||||
| } | ||||
| 
 | ||||
| interface Xput { | ||||
|   type: 'input' | 'output' | 'fee'; | ||||
|   value?: number; | ||||
|   index?: number; | ||||
|   address?: string; | ||||
|   rest?: number; | ||||
|   coinbase?: boolean; | ||||
|   pegin?: boolean; | ||||
|   pegout?: string; | ||||
|   confidential?: boolean; | ||||
| } | ||||
| 
 | ||||
| const lineLimit = 250; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'tx-bowtie-graph', | ||||
|   templateUrl: './tx-bowtie-graph.component.html', | ||||
| @ -20,11 +34,17 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { | ||||
|   @Input() combinedWeight = 100; | ||||
|   @Input() minWeight = 2; //
 | ||||
|   @Input() maxStrands = 24; // number of inputs/outputs to keep fully on-screen.
 | ||||
|   @Input() tooltip = false; | ||||
| 
 | ||||
|   inputData: Xput[]; | ||||
|   outputData: Xput[]; | ||||
|   inputs: SvgLine[]; | ||||
|   outputs: SvgLine[]; | ||||
|   middle: SvgLine; | ||||
|   midWidth: number; | ||||
|   isLiquid: boolean = false; | ||||
|   hoverLine: Xput | void = null; | ||||
|   tooltipPosition = { x: 0, y: 0 }; | ||||
| 
 | ||||
|   gradientColors = { | ||||
|     '': ['#9339f4', '#105fb0'], | ||||
| @ -44,28 +64,68 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { | ||||
|   ngOnInit(): void { | ||||
|     this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet'); | ||||
|     this.gradient = this.gradientColors[this.network]; | ||||
|     this.midWidth = Math.min(50, Math.ceil(this.width / 20)); | ||||
|     this.initGraph(); | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges(): void { | ||||
|     this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet'); | ||||
|     this.gradient = this.gradientColors[this.network]; | ||||
|     this.midWidth = Math.min(50, Math.ceil(this.width / 20)); | ||||
|     this.initGraph(); | ||||
|   } | ||||
| 
 | ||||
|   initGraph(): void { | ||||
|     const totalValue = this.calcTotalValue(this.tx); | ||||
|     const voutWithFee = this.tx.vout.map(v => { return { type: v.scriptpubkey_type === 'fee' ? 'fee' : 'output', value: v?.value }; }); | ||||
|     let voutWithFee = this.tx.vout.map(v => { | ||||
|       return { | ||||
|         type: v.scriptpubkey_type === 'fee' ? 'fee' : 'output', | ||||
|         value: v?.value, | ||||
|         address: v?.scriptpubkey_address || v?.scriptpubkey_type?.toUpperCase(), | ||||
|         pegout: v?.pegout?.scriptpubkey_address, | ||||
|         confidential: (this.isLiquid && v?.value === undefined), | ||||
|       } as Xput; | ||||
|     }); | ||||
| 
 | ||||
|     if (this.tx.fee && !this.isLiquid) { | ||||
|       voutWithFee.unshift({ type: 'fee', value: this.tx.fee }); | ||||
|     } | ||||
|     const outputCount = voutWithFee.length; | ||||
| 
 | ||||
|     this.inputs = this.initLines('in', this.tx.vin.map(v => { return {type: 'input', value: v?.prevout?.value }; }), totalValue, this.maxStrands); | ||||
|     let truncatedInputs = this.tx.vin.map(v => { | ||||
|       return { | ||||
|         type: 'input', | ||||
|         value: v?.prevout?.value, | ||||
|         address: v?.prevout?.scriptpubkey_address || v?.prevout?.scriptpubkey_type?.toUpperCase(), | ||||
|         coinbase: v?.is_coinbase, | ||||
|         pegin: v?.is_pegin, | ||||
|         confidential: (this.isLiquid && v?.prevout?.value === undefined), | ||||
|       } as Xput; | ||||
|     }); | ||||
| 
 | ||||
|     if (truncatedInputs.length > lineLimit) { | ||||
|       const valueOfRest = truncatedInputs.slice(lineLimit).reduce((r, v) => { | ||||
|         return r + (v.value || 0); | ||||
|       }, 0); | ||||
|       truncatedInputs = truncatedInputs.slice(0, lineLimit); | ||||
|       truncatedInputs.push({ type: 'input', value: valueOfRest, rest: this.tx.vin.length - lineLimit }); | ||||
|     } | ||||
|     if (voutWithFee.length > lineLimit) { | ||||
|       const valueOfRest = voutWithFee.slice(lineLimit).reduce((r, v) => { | ||||
|         return r + (v.value || 0); | ||||
|       }, 0); | ||||
|       voutWithFee = voutWithFee.slice(0, lineLimit); | ||||
|       voutWithFee.push({ type: 'output', value: valueOfRest, rest: outputCount - lineLimit }); | ||||
|     } | ||||
| 
 | ||||
|     this.inputData = truncatedInputs; | ||||
|     this.outputData = voutWithFee; | ||||
| 
 | ||||
|     this.inputs = this.initLines('in', truncatedInputs, totalValue, this.maxStrands); | ||||
|     this.outputs = this.initLines('out', voutWithFee, totalValue, this.maxStrands); | ||||
| 
 | ||||
|     this.middle = { | ||||
|       path: `M ${(this.width / 2) - 50} ${(this.height / 2) + 0.5} L ${(this.width / 2) + 50} ${(this.height / 2) + 0.5}`, | ||||
|       path: `M ${(this.width / 2) - this.midWidth} ${(this.height / 2) + 0.5} L ${(this.width / 2) + this.midWidth} ${(this.height / 2) + 0.5}`, | ||||
|       style: `stroke-width: ${this.combinedWeight + 0.5}; stroke: ${this.gradient[1]}` | ||||
|     }; | ||||
|   } | ||||
| @ -95,7 +155,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   initLines(side: 'in' | 'out', xputs: { type: string, value: number | void }[], total: number, maxVisibleStrands: number): SvgLine[] { | ||||
|   initLines(side: 'in' | 'out', xputs: Xput[], total: number, maxVisibleStrands: number): SvgLine[] { | ||||
|     if (!total) { | ||||
|       const weights = xputs.map((put): number => this.combinedWeight / xputs.length); | ||||
|       return this.linesFromWeights(side, xputs, weights, maxVisibleStrands); | ||||
| @ -116,7 +176,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   linesFromWeights(side: 'in' | 'out', xputs: { type: string, value: number | void }[], weights: number[], maxVisibleStrands: number) { | ||||
|   linesFromWeights(side: 'in' | 'out', xputs: Xput[], weights: number[], maxVisibleStrands: number) { | ||||
|     const lines = []; | ||||
|     // actual displayed line thicknesses
 | ||||
|     const minWeights = weights.map((w) => Math.max(this.minWeight - 1, w) + 1); | ||||
| @ -158,7 +218,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { | ||||
| 
 | ||||
|   makePath(side: 'in' | 'out', outer: number, inner: number, weight: number): string { | ||||
|     const start = side === 'in' ? (weight * 0.5) : this.width - (weight * 0.5); | ||||
|     const center =  this.width / 2 + (side === 'in' ? -45 : 45 ); | ||||
|     const center =  this.width / 2 + (side === 'in' ? -(this.midWidth * 0.9) : (this.midWidth * 0.9) ); | ||||
|     const midpoint = (start + center) / 2; | ||||
|     // correct for svg horizontal gradient bug
 | ||||
|     if (Math.round(outer) === Math.round(inner)) { | ||||
| @ -169,9 +229,32 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { | ||||
| 
 | ||||
|   makeStyle(minWeight, type): string { | ||||
|     if (type === 'fee') { | ||||
|       return `stroke-width: ${minWeight}; stroke: url(#fee-gradient)`; | ||||
|       return `stroke-width: ${minWeight}`; | ||||
|     } else { | ||||
|       return `stroke-width: ${minWeight}`; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('pointermove', ['$event']) | ||||
|   onPointerMove(event) { | ||||
|     this.tooltipPosition = { x: event.offsetX, y: event.offsetY }; | ||||
|   } | ||||
| 
 | ||||
|   onHover(event, side, index): void { | ||||
|     if (side === 'input') { | ||||
|       this.hoverLine = { | ||||
|         ...this.inputData[index], | ||||
|         index | ||||
|       }; | ||||
|     } else { | ||||
|       this.hoverLine = { | ||||
|         ...this.outputData[index], | ||||
|         index | ||||
|       }; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   onBlur(event, side, index): void { | ||||
|     this.hoverLine = null; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -7,20 +7,22 @@ import { PreviewsRoutingModule } from './previews.routing.module'; | ||||
| import { TransactionPreviewComponent } from './components/transaction/transaction-preview.component'; | ||||
| import { BlockPreviewComponent } from './components/block/block-preview.component'; | ||||
| import { AddressPreviewComponent } from './components/address/address-preview.component'; | ||||
| import { PoolPreviewComponent } from './components/pool/pool-preview.component'; | ||||
| import { MasterPagePreviewComponent } from './components/master-page-preview/master-page-preview.component'; | ||||
| @NgModule({ | ||||
|   declarations: [ | ||||
|     TransactionPreviewComponent, | ||||
|     BlockPreviewComponent, | ||||
|     AddressPreviewComponent, | ||||
|     PoolPreviewComponent, | ||||
|     MasterPagePreviewComponent, | ||||
|   ], | ||||
|   imports: [ | ||||
|     CommonModule, | ||||
|     SharedModule, | ||||
|     RouterModule, | ||||
|     GraphsModule, | ||||
|     PreviewsRoutingModule, | ||||
|     GraphsModule, | ||||
|   ], | ||||
| }) | ||||
| export class PreviewsModule { } | ||||
|  | ||||
| @ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router'; | ||||
| import { TransactionPreviewComponent } from './components/transaction/transaction-preview.component'; | ||||
| import { BlockPreviewComponent } from './components/block/block-preview.component'; | ||||
| import { AddressPreviewComponent } from './components/address/address-preview.component'; | ||||
| import { PoolPreviewComponent } from './components/pool/pool-preview.component'; | ||||
| import { MasterPagePreviewComponent } from './components/master-page-preview/master-page-preview.component'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
| @ -24,6 +25,10 @@ const routes: Routes = [ | ||||
|         children: [], | ||||
|         component: TransactionPreviewComponent | ||||
|       }, | ||||
|       { | ||||
|         path: 'mining/pool/:slug', | ||||
|         component: PoolPreviewComponent | ||||
|       }, | ||||
|       { | ||||
|         path: 'lightning', | ||||
|         loadChildren: () => import('./lightning/lightning-previews.module').then(m => m.LightningPreviewsModule) | ||||
|  | ||||
| @ -61,6 +61,7 @@ import { FeesBoxComponent } from '../components/fees-box/fees-box.component'; | ||||
| import { DifficultyComponent } from '../components/difficulty/difficulty.component'; | ||||
| import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component'; | ||||
| import { TxBowtieGraphComponent } from '../components/tx-bowtie-graph/tx-bowtie-graph.component'; | ||||
| import { TxBowtieGraphTooltipComponent } from '../components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component'; | ||||
| import { PrivacyPolicyComponent } from '../components/privacy-policy/privacy-policy.component'; | ||||
| import { TrademarkPolicyComponent } from '../components/trademark-policy/trademark-policy.component'; | ||||
| import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component'; | ||||
| @ -134,6 +135,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati | ||||
|     FeesBoxComponent, | ||||
|     DifficultyComponent, | ||||
|     TxBowtieGraphComponent, | ||||
|     TxBowtieGraphTooltipComponent, | ||||
|     TermsOfServiceComponent, | ||||
|     PrivacyPolicyComponent, | ||||
|     TrademarkPolicyComponent, | ||||
| @ -236,6 +238,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati | ||||
|     FeesBoxComponent, | ||||
|     DifficultyComponent, | ||||
|     TxBowtieGraphComponent, | ||||
|     TxBowtieGraphTooltipComponent, | ||||
|     TermsOfServiceComponent, | ||||
|     PrivacyPolicyComponent, | ||||
|     TrademarkPolicyComponent, | ||||
|  | ||||
| @ -61,7 +61,16 @@ const routes = { | ||||
|   }, | ||||
|   mining: { | ||||
|     title: "Mining", | ||||
|     fallbackImg: '/resources/previews/mining.png' | ||||
|     fallbackImg: '/resources/previews/mining.png', | ||||
|     routes: { | ||||
|       pool: { | ||||
|         render: true, | ||||
|         params: 1, | ||||
|         getTitle(path) { | ||||
|           return `Mining Pool: ${path[0]}`; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user