Merge branch 'master' into hotfix/rtlIssues
This commit is contained in:
		
						commit
						aff8f2c41f
					
				| @ -0,0 +1,42 @@ | ||||
| <app-indexing-progress *ngIf="!widget"></app-indexing-progress> | ||||
| 
 | ||||
| <div [class.full-container]="!widget"> | ||||
|   <div *ngIf="!widget" class="card-header mb-0 mb-md-4"> | ||||
|     <div class="d-flex d-md-block align-items-baseline"> | ||||
|       <span i18n="accelerator.acceleration-fees">Acceleration Fees</span> | ||||
|       <button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()"> | ||||
|         <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon> | ||||
|       </button> | ||||
|     </div>   | ||||
| 
 | ||||
|     <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats"> | ||||
|       <div class="btn-group btn-group-toggle" name="radioBasic" [class]="{'disabled': isLoading}"> | ||||
|         <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '24h'"> | ||||
|           <input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" formControlName="dateSpan"> 24H | ||||
|         </label> | ||||
|         <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3d'"> | ||||
|           <input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" formControlName="dateSpan"> 3D | ||||
|         </label> | ||||
|         <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1w'"> | ||||
|           <input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" formControlName="dateSpan"> 1W | ||||
|         </label> | ||||
|         <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1m'"> | ||||
|           <input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" formControlName="dateSpan"> 1M | ||||
|         </label> | ||||
|       </div> | ||||
|     </form> | ||||
|   </div> | ||||
| 
 | ||||
|   <div *ngIf="widget"> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="acceleration.block-fees">Out-of-band Fees Per Block</h5> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <div [class.chart]="!widget" [class.chart-widget]="widget" echarts [initOpts]="chartInitOptions" [options]="chartOptions" | ||||
|     (chartInit)="onChartInit($event)"> | ||||
|   </div> | ||||
|   <div class="text-center loadingGraphs" *ngIf="isLoading"> | ||||
|     <div class="spinner-border text-light"></div> | ||||
|   </div> | ||||
| </div> | ||||
| @ -0,0 +1,74 @@ | ||||
| .card-header { | ||||
|   border-bottom: 0; | ||||
|   font-size: 18px; | ||||
|   @media (min-width: 465px) { | ||||
|     font-size: 20px; | ||||
|   } | ||||
|   @media (min-width: 992px) { | ||||
|     height: 40px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .main-title { | ||||
|   position: relative; | ||||
|   color: #ffffff91; | ||||
|   margin-top: -13px; | ||||
|   font-size: 10px; | ||||
|   text-transform: uppercase; | ||||
|   font-weight: 500; | ||||
|   text-align: center; | ||||
|   padding-bottom: 3px; | ||||
| } | ||||
| 
 | ||||
| .full-container { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   padding: 0px 15px; | ||||
|   width: 100%; | ||||
|   height: calc(100vh - 250px); | ||||
|   @media (min-width: 992px) { | ||||
|     height: calc(100vh - 150px); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .chart { | ||||
|   display: flex; | ||||
|   flex: 1; | ||||
|   width: 100%; | ||||
|   padding-bottom: 20px; | ||||
|   padding-right: 10px; | ||||
|   @media (max-width: 992px) { | ||||
|     padding-bottom: 25px; | ||||
|   } | ||||
|   @media (max-width: 829px) { | ||||
|     padding-bottom: 50px; | ||||
|   } | ||||
|   @media (max-width: 767px) { | ||||
|     padding-bottom: 25px; | ||||
|   } | ||||
|   @media (max-width: 629px) { | ||||
|     padding-bottom: 55px; | ||||
|   } | ||||
|   @media (max-width: 567px) { | ||||
|     padding-bottom: 55px; | ||||
|   } | ||||
| } | ||||
| .chart-widget { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   max-height: 290px; | ||||
| } | ||||
| 
 | ||||
| h5 { | ||||
|   margin-bottom: 10px; | ||||
| } | ||||
| 
 | ||||
| .card-title { | ||||
|   font-size: 1rem; | ||||
|   color: #4a68b9; | ||||
| } | ||||
| 
 | ||||
| .disabled { | ||||
|   pointer-events: none; | ||||
|   opacity: 0.5; | ||||
| } | ||||
| @ -0,0 +1,390 @@ | ||||
| import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnDestroy, OnInit } from '@angular/core'; | ||||
| import { EChartsOption, graphic } from 'echarts'; | ||||
| import { Observable, Subscription, combineLatest } from 'rxjs'; | ||||
| import { map, max, startWith, switchMap, tap } from 'rxjs/operators'; | ||||
| import { ApiService } from '../../../services/api.service'; | ||||
| import { SeoService } from '../../../services/seo.service'; | ||||
| import { formatNumber } from '@angular/common'; | ||||
| import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; | ||||
| import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../../shared/graphs.utils'; | ||||
| import { StorageService } from '../../../services/storage.service'; | ||||
| import { MiningService } from '../../../services/mining.service'; | ||||
| import { ActivatedRoute } from '@angular/router'; | ||||
| import { Acceleration } from '../../../interfaces/node-api.interface'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-acceleration-fees-graph', | ||||
|   templateUrl: './acceleration-fees-graph.component.html', | ||||
|   styleUrls: ['./acceleration-fees-graph.component.scss'], | ||||
|   styles: [` | ||||
|     .loadingGraphs { | ||||
|       position: absolute; | ||||
|       top: 50%; | ||||
|       left: calc(50% - 15px); | ||||
|       z-index: 100; | ||||
|     } | ||||
|   `],
 | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { | ||||
|   @Input() widget: boolean = false; | ||||
|   @Input() right: number | string = 45; | ||||
|   @Input() left: number | string = 75; | ||||
|   @Input() accelerations$: Observable<Acceleration[]>; | ||||
| 
 | ||||
|   miningWindowPreference: string; | ||||
|   radioGroupForm: UntypedFormGroup; | ||||
| 
 | ||||
|   chartOptions: EChartsOption = {}; | ||||
|   chartInitOptions = { | ||||
|     renderer: 'svg', | ||||
|   }; | ||||
| 
 | ||||
|   hrStatsObservable$: Observable<any>; | ||||
|   statsObservable$: Observable<any>; | ||||
|   statsSubscription: Subscription; | ||||
|   isLoading = true; | ||||
|   formatNumber = formatNumber; | ||||
|   timespan = ''; | ||||
|   chartInstance: any = undefined; | ||||
| 
 | ||||
|   currency: string; | ||||
| 
 | ||||
|   constructor( | ||||
|     @Inject(LOCALE_ID) public locale: string, | ||||
|     private seoService: SeoService, | ||||
|     private apiService: ApiService, | ||||
|     private formBuilder: UntypedFormBuilder, | ||||
|     private storageService: StorageService, | ||||
|     private miningService: MiningService, | ||||
|     private route: ActivatedRoute, | ||||
|     private cd: ChangeDetectorRef, | ||||
|   ) { | ||||
|     this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' }); | ||||
|     this.radioGroupForm.controls.dateSpan.setValue('1y'); | ||||
|     this.currency = 'USD'; | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.seoService.setTitle($localize`:@@6c453b11fd7bd159ae30bc381f367bc736d86909:Acceleration Fees`); | ||||
|     this.isLoading = true; | ||||
|     if (this.widget) { | ||||
|       this.miningWindowPreference = '1m'; | ||||
|       this.timespan = this.miningWindowPreference; | ||||
| 
 | ||||
|       this.statsObservable$ = combineLatest([ | ||||
|         (this.accelerations$ || this.apiService.getAccelerationHistory$({ timeframe: this.miningWindowPreference })), | ||||
|         this.apiService.getHistoricalBlockFees$(this.miningWindowPreference), | ||||
|       ]).pipe( | ||||
|         tap(([accelerations, blockFeesResponse]) => { | ||||
|           this.prepareChartOptions(accelerations, blockFeesResponse.body); | ||||
|         }), | ||||
|         map(([accelerations, blockFeesResponse]) => { | ||||
|           return { | ||||
|             avgFeesPaid: accelerations.filter(acc => acc.status === 'completed').reduce((total, acc) => total + acc.feePaid, 0) / accelerations.length | ||||
|           }; | ||||
|         }), | ||||
|       ); | ||||
|     } else { | ||||
|       this.miningWindowPreference = this.miningService.getDefaultTimespan('1w'); | ||||
|       this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); | ||||
|       this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); | ||||
|       this.route.fragment.subscribe((fragment) => { | ||||
|         if (['24h', '3d', '1w', '1m'].indexOf(fragment) > -1) { | ||||
|           this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false }); | ||||
|         } | ||||
|       }); | ||||
|       this.statsObservable$ = combineLatest([ | ||||
|         this.radioGroupForm.get('dateSpan').valueChanges.pipe( | ||||
|           startWith(this.radioGroupForm.controls.dateSpan.value), | ||||
|           switchMap((timespan) => { | ||||
|             this.isLoading = true; | ||||
|             this.storageService.setValue('miningWindowPreference', timespan); | ||||
|             this.timespan = timespan; | ||||
|             return this.apiService.getAccelerationHistory$({}); | ||||
|           }) | ||||
|         ), | ||||
|         this.radioGroupForm.get('dateSpan').valueChanges.pipe( | ||||
|           startWith(this.radioGroupForm.controls.dateSpan.value), | ||||
|           switchMap((timespan) => { | ||||
|             return this.apiService.getHistoricalBlockFees$(timespan); | ||||
|           }) | ||||
|         ) | ||||
|       ]).pipe( | ||||
|         tap(([accelerations, blockFeesResponse]) => { | ||||
|           this.prepareChartOptions(accelerations, blockFeesResponse.body); | ||||
|         }) | ||||
|       ); | ||||
|     } | ||||
|     this.statsSubscription = this.statsObservable$.subscribe(() => { | ||||
|       this.isLoading = false; | ||||
|       this.cd.markForCheck(); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   prepareChartOptions(accelerations, blockFees) { | ||||
|     let title: object; | ||||
| 
 | ||||
|     const blockAccelerations = {}; | ||||
| 
 | ||||
|     for (const acceleration of accelerations) { | ||||
|       if (acceleration.status === 'completed') { | ||||
|         if (!blockAccelerations[acceleration.blockHeight]) { | ||||
|           blockAccelerations[acceleration.blockHeight] = []; | ||||
|         } | ||||
|         blockAccelerations[acceleration.blockHeight].push(acceleration); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     let last = null; | ||||
|     let minValue = Infinity; | ||||
|     let maxValue = 0; | ||||
|     const data = []; | ||||
|     for (const val of blockFees) { | ||||
|       if (last == null) { | ||||
|         last = val.avgHeight; | ||||
|       } | ||||
|       let totalFeeDelta = 0; | ||||
|       let totalFeePaid = 0; | ||||
|       let totalCount = 0; | ||||
|       let blockCount = 0; | ||||
|       while (last <= val.avgHeight) { | ||||
|         blockCount++; | ||||
|         totalFeeDelta += (blockAccelerations[last] || []).reduce((total, acc) => total + acc.feeDelta, 0); | ||||
|         totalFeePaid += (blockAccelerations[last] || []).reduce((total, acc) => total + acc.feePaid, 0); | ||||
|         totalCount += (blockAccelerations[last] || []).length; | ||||
|         last++; | ||||
|       } | ||||
|       minValue = Math.min(minValue, val.avgFees); | ||||
|       maxValue = Math.max(maxValue, val.avgFees); | ||||
|       data.push({ | ||||
|         ...val, | ||||
|         feeDelta: totalFeeDelta, | ||||
|         avgFeePaid: (totalFeePaid / blockCount), | ||||
|         accelerations: totalCount / blockCount, | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     this.chartOptions = { | ||||
|       title: title, | ||||
|       color: [ | ||||
|         '#8F5FF6', | ||||
|         '#6b6b6b', | ||||
|       ], | ||||
|       animation: false, | ||||
|       grid: { | ||||
|         right: this.right, | ||||
|         left: this.left, | ||||
|         bottom: this.widget ? 30 : 80, | ||||
|         top: this.widget ? 20 : (this.isMobile() ? 10 : 50), | ||||
|       }, | ||||
|       tooltip: { | ||||
|         show: !this.isMobile(), | ||||
|         trigger: 'axis', | ||||
|         axisPointer: { | ||||
|           type: 'line' | ||||
|         }, | ||||
|         backgroundColor: 'rgba(17, 19, 31, 1)', | ||||
|         borderRadius: 4, | ||||
|         shadowColor: 'rgba(0, 0, 0, 0.5)', | ||||
|         textStyle: { | ||||
|           color: '#b1b1b1', | ||||
|           align: 'left', | ||||
|         }, | ||||
|         borderColor: '#000', | ||||
|         formatter: function (data) { | ||||
|           if (data.length <= 0) { | ||||
|             return ''; | ||||
|           } | ||||
|           let tooltip = `<b style="color: white; margin-left: 2px">
 | ||||
|             ${formatterXAxis(this.locale, this.timespan, parseInt(data[0].axisValue, 10))}</b><br>`;
 | ||||
| 
 | ||||
|           for (const tick of data.reverse()) { | ||||
|             if (tick.data[1] >= 1_000_000) { | ||||
|               tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1] / 100_000_000, this.locale, '1.0-3')} BTC<br>`; | ||||
|             } else { | ||||
|               tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')} sats<br>`; | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|           if (['24h', '3d'].includes(this.timespan)) { | ||||
|             tooltip += `<small>` + $localize`At block: ${data[0].data[2]}` + `</small>`; | ||||
|           } else { | ||||
|             tooltip += `<small>` + $localize`Around block: ${data[0].data[2]}` + `</small>`; | ||||
|           } | ||||
| 
 | ||||
|           return tooltip; | ||||
|         }.bind(this) | ||||
|       }, | ||||
|       xAxis: data.length === 0 ? undefined : | ||||
|       { | ||||
|         name: this.widget ? undefined : formatterXAxisLabel(this.locale, this.timespan), | ||||
|         nameLocation: 'middle', | ||||
|         nameTextStyle: { | ||||
|           padding: [10, 0, 0, 0], | ||||
|         }, | ||||
|         type: 'category', | ||||
|         boundaryGap: false, | ||||
|         axisLine: { onZero: true }, | ||||
|         axisLabel: { | ||||
|           formatter: val => formatterXAxisTimeCategory(this.locale, this.timespan, parseInt(val, 10)), | ||||
|           align: 'center', | ||||
|           fontSize: 11, | ||||
|           lineHeight: 12, | ||||
|           hideOverlap: true, | ||||
|           padding: [0, 5], | ||||
|         }, | ||||
|       }, | ||||
|       legend: { | ||||
|         data: [ | ||||
|           { | ||||
|             name: 'In-band fees per block', | ||||
|             inactiveColor: 'rgb(110, 112, 121)', | ||||
|             textStyle: { | ||||
|               color: 'white', | ||||
|             }, | ||||
|             icon: 'roundRect', | ||||
|           }, | ||||
|           { | ||||
|             name: 'Out-of-band fees per block', | ||||
|             inactiveColor: 'rgb(110, 112, 121)', | ||||
|             textStyle: { | ||||
|               color: 'white', | ||||
|             }, | ||||
|             icon: 'roundRect', | ||||
|           }, | ||||
|         ], | ||||
|         selected: { | ||||
|           'In-band fees per block': false, | ||||
|           'Out-of-band fees per block': true, | ||||
|         }, | ||||
|         show: !this.widget, | ||||
|       }, | ||||
|       yAxis: data.length === 0 ? undefined : [ | ||||
|         { | ||||
|           type: 'value', | ||||
|           axisLabel: { | ||||
|             color: 'rgb(110, 112, 121)', | ||||
|             formatter: (val) => { | ||||
|               if (val >= 100_000) { | ||||
|                 return `${(val / 100_000_000).toFixed(3)} BTC`; | ||||
|               } else { | ||||
|                 return `${val} sats`; | ||||
|               } | ||||
|             } | ||||
|           }, | ||||
|           splitLine: { | ||||
|             lineStyle: { | ||||
|               type: 'dotted', | ||||
|               color: '#ffffff66', | ||||
|               opacity: 0.25, | ||||
|             } | ||||
|           }, | ||||
|         }, | ||||
|         { | ||||
|           type: 'value', | ||||
|           position: 'right', | ||||
|           axisLabel: { | ||||
|             color: 'rgb(110, 112, 121)', | ||||
|             formatter: function(val) { | ||||
|               return `${val}`; | ||||
|             }.bind(this) | ||||
|           }, | ||||
|           splitLine: { | ||||
|             show: false, | ||||
|           }, | ||||
|         }, | ||||
|       ], | ||||
|       series: data.length === 0 ? undefined : [ | ||||
|         { | ||||
|           legendHoverLink: false, | ||||
|           zlevel: 1, | ||||
|           name: 'Out-of-band fees per block', | ||||
|           data: data.map(block =>  [block.timestamp * 1000, block.avgFeePaid, block.avgHeight]), | ||||
|           stack: 'Total', | ||||
|           type: 'bar', | ||||
|           barWidth: '100%', | ||||
|           large: true, | ||||
|         }, | ||||
|         { | ||||
|           legendHoverLink: false, | ||||
|           zlevel: 0, | ||||
|           name: 'In-band fees per block', | ||||
|           data: data.map(block =>  [block.timestamp * 1000, block.avgFees, block.avgHeight]), | ||||
|           stack: 'Total', | ||||
|           type: 'bar', | ||||
|           barWidth: '100%', | ||||
|           large: true, | ||||
|         }, | ||||
|       ], | ||||
|       dataZoom: (this.widget || data.length === 0 )? undefined : [{ | ||||
|         type: 'inside', | ||||
|         realtime: true, | ||||
|         zoomLock: true, | ||||
|         maxSpan: 100, | ||||
|         minSpan: 5, | ||||
|         moveOnMouseMove: false, | ||||
|       }, { | ||||
|         showDetail: false, | ||||
|         show: true, | ||||
|         type: 'slider', | ||||
|         brushSelect: false, | ||||
|         realtime: true, | ||||
|         left: 20, | ||||
|         right: 15, | ||||
|         selectedDataBackground: { | ||||
|           lineStyle: { | ||||
|             color: '#fff', | ||||
|             opacity: 0.45, | ||||
|           }, | ||||
|           areaStyle: { | ||||
|             opacity: 0, | ||||
|           } | ||||
|         }, | ||||
|       }], | ||||
|       visualMap: { | ||||
|         type: 'continuous', | ||||
|         min: minValue, | ||||
|         max: maxValue, | ||||
|         dimension: 1, | ||||
|         seriesIndex: 1, | ||||
|         show: false, | ||||
|         inRange: { | ||||
|           color: ['#F4511E7f', '#FB8C007f', '#FFB3007f', '#FDD8357f', '#7CB3427f'].reverse() // Gradient color range
 | ||||
|         } | ||||
|       }, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   onChartInit(ec) { | ||||
|     this.chartInstance = ec; | ||||
|   } | ||||
| 
 | ||||
|   isMobile() { | ||||
|     return (window.innerWidth <= 767.98); | ||||
|   } | ||||
| 
 | ||||
|   onSaveChart() { | ||||
|     // @ts-ignore
 | ||||
|     const prevBottom = this.chartOptions.grid.bottom; | ||||
|     const now = new Date(); | ||||
|     // @ts-ignore
 | ||||
|     this.chartOptions.grid.bottom = 40; | ||||
|     this.chartOptions.backgroundColor = '#11131f'; | ||||
|     this.chartInstance.setOption(this.chartOptions); | ||||
|     download(this.chartInstance.getDataURL({ | ||||
|       pixelRatio: 2, | ||||
|       excludeComponents: ['dataZoom'], | ||||
|     }), `acceleration-fees-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`); | ||||
|     // @ts-ignore
 | ||||
|     this.chartOptions.grid.bottom = prevBottom; | ||||
|     this.chartOptions.backgroundColor = 'none'; | ||||
|     this.chartInstance.setOption(this.chartOptions); | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|     if (this.statsSubscription) { | ||||
|       this.statsSubscription.unsubscribe(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,53 @@ | ||||
| <div class="stats-wrapper" *ngIf="accelerationStats$ | async as stats; else loading"> | ||||
|   <div class="stats-container"> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="address.transactions">Transactions</h5> | ||||
|       <div class="card-text"> | ||||
|         <div>{{ stats.count }}</div> | ||||
|         <div class="symbol" i18n="accelerator.total-accelerated">accelerated</div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="accelerator.out-of-band-fees">Out-of-band Fees</h5> | ||||
|       <div class="card-text"> | ||||
|         <div>{{ stats.totalFeesPaid / 100_000_000 | amountShortener: 4 }} <span class="symbol" i18n="shared.btc|BTC">BTC</span></div> | ||||
|         <span class="fiat"> | ||||
|           <app-fiat [value]="stats.totalFeesPaid"></app-fiat> | ||||
|         </span> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="accelerator.success-rate">Success rate</h5> | ||||
|       <div class="card-text"> | ||||
|         <div>{{ stats.successRate.toFixed(2) }} %</div> | ||||
|         <div class="symbol" i18n="accelerator.mined-next-block">mined</div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
| <ng-template #loading> | ||||
|   <div class="stats-container loading-container"> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="address.transactions">Transactions</h5> | ||||
|       <div class="card-text"> | ||||
|         <div class="skeleton-loader"></div> | ||||
|         <div class="skeleton-loader"></div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="accelerator.out-of-band-fees">Out-of-band Fees</h5> | ||||
|       <div class="card-text"> | ||||
|         <div class="skeleton-loader"></div> | ||||
|         <div class="skeleton-loader"></div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="accelerator.success-rate">Success rate</h5> | ||||
|       <div class="card-text"> | ||||
|         <div class="skeleton-loader"></div> | ||||
|         <div class="skeleton-loader"></div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </ng-template> | ||||
| @ -0,0 +1,88 @@ | ||||
| .card-title { | ||||
|   color: #4a68b9; | ||||
|   font-size: 10px; | ||||
|   margin-bottom: 4px;   | ||||
|   font-size: 1rem; | ||||
|   overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
|   white-space: nowrap; | ||||
| } | ||||
| 
 | ||||
| .card-text { | ||||
|   font-size: 22px; | ||||
|   span { | ||||
|     font-size: 11px; | ||||
|     position: relative; | ||||
|     top: -2px; | ||||
|     display: inline-flex; | ||||
|   } | ||||
|   .green-color { | ||||
|     display: block; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .stats-container { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   @media (min-width: 376px) { | ||||
|     flex-direction: row; | ||||
|   }   | ||||
|   .item { | ||||
|     max-width: 150px; | ||||
|     margin: 0; | ||||
|     width: -webkit-fill-available; | ||||
|     @media (min-width: 376px) { | ||||
|       margin: 0 auto 0px; | ||||
|     }     | ||||
|     &:first-child{ | ||||
|       display: none; | ||||
|       @media (min-width: 485px) { | ||||
|         display: block; | ||||
|       }     | ||||
|       @media (min-width: 768px) { | ||||
|         display: none; | ||||
|       }     | ||||
|       @media (min-width: 992px) { | ||||
|         display: block; | ||||
|       }     | ||||
|     } | ||||
|     &:last-child { | ||||
|       margin-bottom: 0; | ||||
|     } | ||||
|     .card-text span { | ||||
|       color: #ffffff66; | ||||
|       font-size: 12px; | ||||
|       top: 0px; | ||||
|     } | ||||
|     .fee-text{ | ||||
|       border-bottom: 1px solid #ffffff1c; | ||||
|       width: fit-content; | ||||
|       margin: auto; | ||||
|       line-height: 1.45; | ||||
|       padding: 0px 2px; | ||||
|     } | ||||
|     .fiat { | ||||
|       display: block; | ||||
|       font-size: 14px !important; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .loading-container{ | ||||
|   min-height: 76px; | ||||
| } | ||||
| 
 | ||||
| .card-text { | ||||
|   .skeleton-loader { | ||||
|     width: 100%; | ||||
|     display: block; | ||||
|     &:first-child { | ||||
|       max-width: 90px; | ||||
|       margin: 15px auto 3px; | ||||
|     } | ||||
|     &:last-child { | ||||
|       margin: 10px auto 3px; | ||||
|       max-width: 55px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,46 @@ | ||||
| import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; | ||||
| import { Observable, of } from 'rxjs'; | ||||
| import { switchMap } from 'rxjs/operators'; | ||||
| import { ApiService } from '../../../services/api.service'; | ||||
| import { StateService } from '../../../services/state.service'; | ||||
| import { Acceleration } from '../../../interfaces/node-api.interface'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-acceleration-stats', | ||||
|   templateUrl: './acceleration-stats.component.html', | ||||
|   styleUrls: ['./acceleration-stats.component.scss'], | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class AccelerationStatsComponent implements OnInit { | ||||
|   @Input() timespan: '24h' | '1w' | '1m' = '24h'; | ||||
|   @Input() accelerations$: Observable<Acceleration[]>; | ||||
|   public accelerationStats$: Observable<any>; | ||||
| 
 | ||||
|   constructor( | ||||
|     private apiService: ApiService, | ||||
|     private stateService: StateService, | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.accelerationStats$ = this.accelerations$.pipe( | ||||
|       switchMap(accelerations => { | ||||
|         let totalFeesPaid = 0; | ||||
|         let totalSucceeded = 0; | ||||
|         let totalCanceled = 0; | ||||
|         for (const acceleration of accelerations) { | ||||
|           if (acceleration.status === 'completed') { | ||||
|             totalSucceeded++; | ||||
|             totalFeesPaid += acceleration.feePaid || 0; | ||||
|           } else if (acceleration.status === 'failed') { | ||||
|             totalCanceled++; | ||||
|           } | ||||
|         } | ||||
|         return of({ | ||||
|           count: totalSucceeded, | ||||
|           totalFeesPaid, | ||||
|           successRate: (totalSucceeded + totalCanceled > 0) ? ((totalSucceeded / (totalSucceeded + totalCanceled)) * 100) : 0.0, | ||||
|         }); | ||||
|       }) | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,91 @@ | ||||
| <div class="container-xl" style="min-height: 335px" [class.widget]="widget" [class.full-height]="!widget"> | ||||
|   <h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Accelerations</h1> | ||||
|   <div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div> | ||||
| 
 | ||||
|   <div class="clearfix"></div> | ||||
| 
 | ||||
|   <div style="min-height: 295px" *ngIf="accelerationList$ | async as accelerations"> | ||||
|     <table *ngIf="!accelerations || accelerations.length; else noData" class="table table-borderless table-fixed"> | ||||
|       <thead> | ||||
|         <th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th> | ||||
|         <ng-container *ngIf="pending"> | ||||
|           <th class="fee-rate text-right" i18n="transaction.fee|Transaction fee">Fee Rate</th> | ||||
|           <th class="bid text-right" i18n="transaction.fee|Transaction fee">Acceleration Bid</th> | ||||
|           <th class="time text-right" i18n="accelerator.block">Requested</th> | ||||
|         </ng-container> | ||||
|         <ng-container *ngIf="!pending"> | ||||
|           <th class="fee text-right" i18n="transaction.fee|Transaction fee">Out-of-band Fee</th> | ||||
|           <th class="block text-right" i18n="accelerator.block">Block</th> | ||||
|           <th class="status text-right" i18n="transaction.status|Transaction Status">Status</th> | ||||
|         </ng-container> | ||||
|       </thead> | ||||
|       <tbody *ngIf="accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''"> | ||||
|         <tr *ngFor="let acceleration of accelerations; let i= index;"> | ||||
|           <td class="txid text-left"> | ||||
|             <a [routerLink]="['/tx' | relativeUrl, acceleration.txid]"> | ||||
|               <app-truncate [text]="acceleration.txid" [lastChars]="5"></app-truncate> | ||||
|             </a> | ||||
|           </td> | ||||
|           <ng-container *ngIf="pending"> | ||||
|             <td class="fee-rate text-right"> | ||||
|               <app-fee-rate [fee]="acceleration.effectiveFee" [weight]="acceleration.effectiveVsize * 4"></app-fee-rate> | ||||
|             </td> | ||||
|             <td class="bid text-right"> | ||||
|               {{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> | ||||
|             </td> | ||||
|             <td class="time text-right"> | ||||
|               <app-time kind="since" [time]="acceleration.added" [fastRender]="true"></app-time> | ||||
|             </td> | ||||
|           </ng-container> | ||||
|           <ng-container *ngIf="!pending"> | ||||
|             <td *ngIf="acceleration.feePaid" class="fee text-right"> | ||||
|               {{ (acceleration.feePaid) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> | ||||
|             </td> | ||||
|             <td *ngIf="!acceleration.feePaid" class="fee text-right"> | ||||
|               ~ | ||||
|             </td> | ||||
|             <td class="block text-right"> | ||||
|               <a [routerLink]="['/block' | relativeUrl, acceleration.blockHeight]">{{ acceleration.blockHeight }}</a> | ||||
|             </td> | ||||
|             <td class="status text-right"> | ||||
|               <span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="transaction.rbf.mined">Pending</span> | ||||
|               <span *ngIf="acceleration.status === 'mined' || acceleration.status === 'completed'" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span> | ||||
|               <span *ngIf="acceleration.status === 'failed'" class="badge badge-danger" i18n="accelerator.canceled">Canceled</span> | ||||
|             </td> | ||||
|           </ng-container> | ||||
|         </tr> | ||||
|       </tbody> | ||||
|       <ng-template #skeleton> | ||||
|         <tbody> | ||||
|           <tr *ngFor="let item of skeletonLines"> | ||||
|             <td class="txid text-left"> | ||||
|               <span class="skeleton-loader" style="max-width: 75px"></span> | ||||
|             </td> | ||||
|             <td class="fee text-right"> | ||||
|               <span class="skeleton-loader" style="max-width: 75px"></span> | ||||
|             </td> | ||||
|             <td class="fee-delta text-right"> | ||||
|               <span class="skeleton-loader" style="max-width: 75px"></span> | ||||
|             </td> | ||||
|             <td class="status text-right"> | ||||
|               <span class="skeleton-loader" style="max-width: 75px"></span> | ||||
|             </td> | ||||
|           </tr> | ||||
|         </tbody> | ||||
|       </ng-template> | ||||
|     </table> | ||||
| 
 | ||||
|     <ng-template [ngIf]="!widget"> | ||||
|       <div class="clearfix"></div> | ||||
|       <br> | ||||
|     </ng-template> | ||||
|   </div> | ||||
| 
 | ||||
|   <ng-template #noData> | ||||
|     <div class="no-data"> | ||||
|       <span *ngIf="pending" i18n="accelerations.no-accelerations">There are no active accelerations</span> | ||||
|       <span *ngIf="!pending" i18n="accelerations.no-accelerations">There are no recent accelerations</span> | ||||
|     </div> | ||||
|   </ng-template> | ||||
|    | ||||
| </div> | ||||
| @ -0,0 +1,125 @@ | ||||
| .spinner-border { | ||||
|   height: 25px; | ||||
|   width: 25px; | ||||
|   margin-top: 13px; | ||||
| } | ||||
| 
 | ||||
| .container-xl { | ||||
|   max-width: 1400px; | ||||
| } | ||||
| .container-xl.widget { | ||||
|   padding-left: 0px; | ||||
|   padding-bottom: 0px; | ||||
| } | ||||
| .container-xl.legacy { | ||||
|   max-width: 1140px; | ||||
| } | ||||
| 
 | ||||
| .container { | ||||
|   max-width: 100%; | ||||
| } | ||||
| 
 | ||||
| tr, td, th { | ||||
|   border: 0px; | ||||
|   padding-top: 0.65rem !important; | ||||
|   padding-bottom: 0.8rem !important; | ||||
| 
 | ||||
|   .difference { | ||||
|     margin-left: 0.5em; | ||||
| 
 | ||||
|     &.positive { | ||||
|       color: rgb(66, 183, 71); | ||||
|     } | ||||
|     &.negative { | ||||
|       color: rgb(183, 66, 66); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .clear-link { | ||||
|   color: white; | ||||
| } | ||||
| 
 | ||||
| .disabled { | ||||
|   pointer-events: none; | ||||
|   opacity: 0.5; | ||||
| } | ||||
| 
 | ||||
| .progress { | ||||
|   background-color: #2d3348; | ||||
| } | ||||
| 
 | ||||
| .txid { | ||||
|   width: 25%; | ||||
|   @media (max-width: 1100px) { | ||||
|     padding-right: 10px; | ||||
|   } | ||||
|   @media (max-width: 875px) { | ||||
|     display: none; | ||||
|   } | ||||
|   overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
|   white-space: nowrap; | ||||
|   max-width: 30%; | ||||
| } | ||||
| 
 | ||||
| .fee { | ||||
|   width: 35%; | ||||
| } | ||||
| 
 | ||||
| .block { | ||||
|   width: 20%; | ||||
| } | ||||
| 
 | ||||
| .bid { | ||||
|   width: 30%; | ||||
| } | ||||
| 
 | ||||
| .time { | ||||
|   width: 25%; | ||||
| } | ||||
| 
 | ||||
| .status { | ||||
|   width: 20% | ||||
| } | ||||
| 
 | ||||
| /* Tooltip text */ | ||||
| .tooltip-custom { | ||||
|   position: relative;  | ||||
| } | ||||
| 
 | ||||
| .tooltip-custom .tooltiptext { | ||||
|   visibility: hidden; | ||||
|   color: #fff; | ||||
|   text-align: center; | ||||
|   padding: 5px 0; | ||||
|   border-radius: 6px; | ||||
|   position: absolute; | ||||
|   z-index: 1; | ||||
|   top: -40px; | ||||
|   left: 0; | ||||
| } | ||||
| 
 | ||||
| /* Show the tooltip text when you mouse over the tooltip container */ | ||||
| .tooltip-custom:hover .tooltiptext { | ||||
|   visibility: visible; | ||||
| } | ||||
| 
 | ||||
| .scriptmessage { | ||||
|   overflow: hidden; | ||||
| 	display: inline-block; | ||||
| 	text-overflow: ellipsis; | ||||
| 	vertical-align: middle; | ||||
| 	max-width: 50vw; | ||||
|   text-align: left; | ||||
| } | ||||
| 
 | ||||
| .no-data { | ||||
|   color: rgba(255, 255, 255, 0.4); | ||||
|   display: flex; | ||||
|   height: 280px; | ||||
|   width: 100%; | ||||
|   flex-direction: row; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
| } | ||||
| @ -0,0 +1,71 @@ | ||||
| import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core'; | ||||
| import { Observable, catchError, of, switchMap, tap } from 'rxjs'; | ||||
| import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; | ||||
| import { ApiService } from '../../../services/api.service'; | ||||
| import { StateService } from '../../../services/state.service'; | ||||
| import { WebsocketService } from '../../../services/websocket.service'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-accelerations-list', | ||||
|   templateUrl: './accelerations-list.component.html', | ||||
|   styleUrls: ['./accelerations-list.component.scss'], | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class AccelerationsListComponent implements OnInit { | ||||
|   @Input() widget: boolean = false; | ||||
|   @Input() pending: boolean = false; | ||||
|   @Input() accelerations$: Observable<Acceleration[]>; | ||||
| 
 | ||||
|   accelerationList$: Observable<Acceleration[]> = undefined; | ||||
| 
 | ||||
|   isLoading = true; | ||||
|   paginationMaxSize: number; | ||||
|   page = 1; | ||||
|   lastPage = 1; | ||||
|   maxSize = window.innerWidth <= 767.98 ? 3 : 5; | ||||
|   skeletonLines: number[] = []; | ||||
| 
 | ||||
|   constructor( | ||||
|     private apiService: ApiService, | ||||
|     private websocketService: WebsocketService, | ||||
|     public stateService: StateService, | ||||
|     private cd: ChangeDetectorRef, | ||||
|   ) { | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     if (!this.widget) { | ||||
|       this.websocketService.want(['blocks']); | ||||
|     } | ||||
| 
 | ||||
|     this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()]; | ||||
|     this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5; | ||||
| 
 | ||||
|     const accelerationObservable$ = this.accelerations$ || (this.pending ? this.apiService.getAccelerations$() : this.apiService.getAccelerationHistory$({ timeframe: '1m' })); | ||||
|     this.accelerationList$ = accelerationObservable$.pipe( | ||||
|       switchMap(accelerations => { | ||||
|         if (this.pending) { | ||||
|           for (const acceleration of accelerations) { | ||||
|             acceleration.status = acceleration.status || 'accelerating'; | ||||
|           } | ||||
|         } | ||||
|         if (this.widget) { | ||||
|           return of(accelerations.slice(-6).reverse()); | ||||
|         } else { | ||||
|           return of(accelerations.reverse()); | ||||
|         } | ||||
|       }), | ||||
|       catchError((err) => { | ||||
|         this.isLoading = false; | ||||
|         return of([]); | ||||
|       }), | ||||
|       tap(() => { | ||||
|         this.isLoading = false; | ||||
|       }) | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   trackByBlock(index: number, block: BlockExtended): number { | ||||
|     return block.height; | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,93 @@ | ||||
| <app-indexing-progress></app-indexing-progress> | ||||
| 
 | ||||
| <div class="container-xl dashboard-container"> | ||||
| 
 | ||||
|   <div class="row row-cols-1 row-cols-md-2"> | ||||
| 
 | ||||
|     <!-- pending stats --> | ||||
|     <div class="col"> | ||||
|       <div class="main-title"> | ||||
|         <span [attr.data-cy]="'pending-accelerations'" i18n="accelerator.pending-accelerations">Active accelerations</span> | ||||
|       </div> | ||||
|       <div class="card-wrapper"> | ||||
|         <div class="card"> | ||||
|           <div class="card-body more-padding"> | ||||
|             <app-pending-stats [accelerations$]="pendingAccelerations$"></app-pending-stats> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- 1m stats --> | ||||
|     <div class="col"> | ||||
|       <div class="main-title"> | ||||
|         <span [attr.data-cy]="'acceleration-stats'" i18n="accelerator.acceleration-stats">Acceleration stats</span>  | ||||
|         <span style="font-size: xx-small" i18n="mining.144-blocks">(1 month)</span> | ||||
|       </div> | ||||
|       <div class="card-wrapper"> | ||||
|         <div class="card"> | ||||
|           <div class="card-body more-padding"> | ||||
|             <app-acceleration-stats timespan="1m" [accelerations$]="minedAccelerations$"></app-acceleration-stats> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Next block visualization --> | ||||
|     <div class="col" style="margin-bottom: 1.47rem"> | ||||
|       <div class="card"> | ||||
|         <div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2"> | ||||
|           <div class="mempool-block-wrapper"> | ||||
|             <app-mempool-block-overview [index]="0" [overrideColors]="getAcceleratorColor"></app-mempool-block-overview> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- acceleration fees graph --> | ||||
|     <div class="col" style="margin-bottom: 1.47rem"> | ||||
|       <div class="card graph-card"> | ||||
|         <div class="card-body pl-2 pr-2"> | ||||
|           <app-acceleration-fees-graph [attr.data-cy]="'acceleration-fees'" [widget]=true [accelerations$]="accelerations$"></app-acceleration-fees-graph> | ||||
|           <div class="mt-1"><a [attr.data-cy]="'acceleration-fees-view-more'" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Avg block fees graph --> | ||||
|     <!-- <div class="col" style="margin-bottom: 1.47rem"> | ||||
|       <div class="card"> | ||||
|         <div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2"> | ||||
|           <app-block-fee-rates-graph [attr.data-cy]="'hashrate-graph'" [widget]="true"></app-block-fee-rates-graph> | ||||
|           <div class="mt-1"><a [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" fragment="1m" i18n="dashboard.view-more">View more »</a></div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> --> | ||||
| 
 | ||||
|     <!-- Recent Accelerations List --> | ||||
|     <div class="col"> | ||||
|       <div class="card list-card"> | ||||
|         <div class="card-body"> | ||||
|           <div class="title-link"> | ||||
|             <h5 class="card-title d-inline" i18n="dashboard.recent-accelerations">Active Accelerations</h5> | ||||
|           </div> | ||||
|           <app-accelerations-list [attr.data-cy]="'pending-accelerations'" [widget]=true [pending]="true" [accelerations$]="pendingAccelerations$"></app-accelerations-list> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Confirmed List --> | ||||
|     <div class="col"> | ||||
|       <div class="card list-card"> | ||||
|         <div class="card-body"> | ||||
|           <a class="title-link" href="" [routerLink]="['/acceleration-list' | relativeUrl]"> | ||||
|             <h5 class="card-title d-inline" i18n="dashboard.recent-accelerations">Recent Accelerations</h5> | ||||
|             <span> </span> | ||||
|             <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: #4a68b9"></fa-icon> | ||||
|           </a> | ||||
|           <app-accelerations-list [attr.data-cy]="'recent-accelerations'" [widget]=true [accelerations$]="minedAccelerations$"></app-accelerations-list> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| @ -0,0 +1,148 @@ | ||||
| .dashboard-container { | ||||
|   text-align: center; | ||||
|   margin-top: 0.5rem; | ||||
|   .col { | ||||
|     margin-bottom: 1.5rem; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .card { | ||||
|   background-color: #1d1f31; | ||||
| } | ||||
| 
 | ||||
| .graph-card { | ||||
|   height: 100%; | ||||
|   @media (min-width: 992px) { | ||||
|     height: 385px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .card-title { | ||||
|   font-size: 1rem; | ||||
|   color: #4a68b9; | ||||
| } | ||||
| .card-title > a { | ||||
|   color: #4a68b9; | ||||
| } | ||||
| 
 | ||||
| .card-body.pool-ranking { | ||||
|   padding: 1.25rem 0.25rem 0.75rem 0.25rem; | ||||
| } | ||||
| .card-text { | ||||
|   font-size: 22px; | ||||
| } | ||||
| 
 | ||||
| #blockchain-container { | ||||
|   position: relative; | ||||
|   overflow-x: scroll; | ||||
|   overflow-y: hidden; | ||||
|   scrollbar-width: none; | ||||
|   -ms-overflow-style: none; | ||||
| } | ||||
| 
 | ||||
| #blockchain-container::-webkit-scrollbar { | ||||
|   display: none; | ||||
| } | ||||
| 
 | ||||
| .fade-border { | ||||
|   -webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 80%, transparent 100%) | ||||
| } | ||||
| 
 | ||||
| .main-title { | ||||
|   position: relative; | ||||
|   color: #ffffff91; | ||||
|   margin-top: -13px; | ||||
|   font-size: 10px; | ||||
|   text-transform: uppercase; | ||||
|   font-weight: 500; | ||||
|   text-align: center; | ||||
|   padding-bottom: 3px; | ||||
| } | ||||
| 
 | ||||
| .more-padding { | ||||
|   padding: 24px 20px !important; | ||||
| } | ||||
| 
 | ||||
| .card-wrapper { | ||||
|   .card { | ||||
|     height: auto !important; | ||||
|   } | ||||
|   .card-body { | ||||
|     display: flex; | ||||
|     flex: inherit; | ||||
|     text-align: center; | ||||
|     flex-direction: column; | ||||
|     justify-content: space-around; | ||||
|     padding: 22px 20px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .skeleton-loader { | ||||
|   width: 100%; | ||||
|   display: block; | ||||
|   &:first-child { | ||||
|     max-width: 90px; | ||||
|     margin: 15px auto 3px; | ||||
|   } | ||||
|   &:last-child { | ||||
|     margin: 10px auto 3px; | ||||
|     max-width: 55px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .card-text { | ||||
|   font-size: 22px; | ||||
| } | ||||
| 
 | ||||
| .title-link, .title-link:hover, .title-link:focus, .title-link:active { | ||||
|   display: block; | ||||
|   margin-bottom: 10px; | ||||
|   text-decoration: none; | ||||
|   color: inherit; | ||||
| } | ||||
| 
 | ||||
| .lastest-blocks-table { | ||||
|   width: 100%; | ||||
|   text-align: left; | ||||
|   tr, td, th { | ||||
|     border: 0px; | ||||
|     padding-top: 0.65rem !important; | ||||
|     padding-bottom: 0.8rem !important; | ||||
|   } | ||||
|   .table-cell-height { | ||||
|     width: 25%; | ||||
|   } | ||||
|   .table-cell-fee { | ||||
|     width: 25%; | ||||
|     text-align: right; | ||||
|   } | ||||
|   .table-cell-pool { | ||||
|     text-align: left; | ||||
|     width: 30%; | ||||
| 
 | ||||
|     @media (max-width: 875px) { | ||||
|       display: none; | ||||
|     } | ||||
| 
 | ||||
|     .pool-name { | ||||
|       margin-left: 1em; | ||||
|     } | ||||
|   } | ||||
|   .table-cell-acceleration-count { | ||||
|     text-align: right; | ||||
|     width: 20%; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .card { | ||||
|   height: 385px; | ||||
| } | ||||
| .list-card { | ||||
|   height: 410px; | ||||
| } | ||||
| 
 | ||||
| .mempool-block-wrapper { | ||||
|   max-height: 380px; | ||||
|   max-width: 380px; | ||||
|   margin: auto; | ||||
| } | ||||
| @ -0,0 +1,122 @@ | ||||
| import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; | ||||
| import { SeoService } from '../../../services/seo.service'; | ||||
| import { WebsocketService } from '../../../services/websocket.service'; | ||||
| import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; | ||||
| import { StateService } from '../../../services/state.service'; | ||||
| import { Observable, Subject, catchError, combineLatest, distinctUntilChanged, interval, map, of, share, startWith, switchMap, tap } from 'rxjs'; | ||||
| import { ApiService } from '../../../services/api.service'; | ||||
| import { Color } from '../../block-overview-graph/sprite-types'; | ||||
| import { hexToColor } from '../../block-overview-graph/utils'; | ||||
| import TxView from '../../block-overview-graph/tx-view'; | ||||
| import { feeLevels, mempoolFeeColors } from '../../../app.constants'; | ||||
| 
 | ||||
| const acceleratedColor: Color = hexToColor('8F5FF6'); | ||||
| const normalColors = mempoolFeeColors.map(hex => hexToColor(hex + '5F')); | ||||
| 
 | ||||
| interface AccelerationBlock extends BlockExtended { | ||||
|   accelerationCount: number, | ||||
| } | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-accelerator-dashboard', | ||||
|   templateUrl: './accelerator-dashboard.component.html', | ||||
|   styleUrls: ['./accelerator-dashboard.component.scss'], | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class AcceleratorDashboardComponent implements OnInit { | ||||
|   blocks$: Observable<AccelerationBlock[]>; | ||||
|   accelerations$: Observable<Acceleration[]>; | ||||
|   pendingAccelerations$: Observable<Acceleration[]>; | ||||
|   minedAccelerations$: Observable<Acceleration[]>; | ||||
|   loadingBlocks: boolean = true; | ||||
| 
 | ||||
|   constructor( | ||||
|     private seoService: SeoService, | ||||
|     private websocketService: WebsocketService, | ||||
|     private apiService: ApiService, | ||||
|     private stateService: StateService, | ||||
|   ) { | ||||
|     this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Accelerator Dashboard`); | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.websocketService.want(['blocks', 'mempool-blocks', 'stats']); | ||||
| 
 | ||||
|     this.pendingAccelerations$ = interval(30000).pipe( | ||||
|       startWith(true), | ||||
|       switchMap(() => { | ||||
|         return this.apiService.getAccelerations$(); | ||||
|       }), | ||||
|       catchError((e) => { | ||||
|         return of([]); | ||||
|       }), | ||||
|       share(), | ||||
|     ); | ||||
| 
 | ||||
|     this.accelerations$ = this.stateService.chainTip$.pipe( | ||||
|       distinctUntilChanged(), | ||||
|       switchMap((chainTip) => { | ||||
|         return this.apiService.getAccelerationHistory$({ timeframe: '1m' }); | ||||
|       }), | ||||
|       catchError((e) => { | ||||
|         return of([]); | ||||
|       }), | ||||
|       share(), | ||||
|     ); | ||||
| 
 | ||||
|     this.minedAccelerations$ = this.accelerations$.pipe( | ||||
|       map(accelerations => { | ||||
|         return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status)) | ||||
|       }) | ||||
|     ); | ||||
| 
 | ||||
|     this.blocks$ = combineLatest([ | ||||
|       this.accelerations$, | ||||
|       this.stateService.blocks$.pipe( | ||||
|         switchMap((blocks) => { | ||||
|           if (this.stateService.env.MINING_DASHBOARD === true) { | ||||
|             for (const block of blocks) { | ||||
|               // @ts-ignore: Need to add an extra field for the template
 | ||||
|               block.extras.pool.logo = `/resources/mining-pools/` + | ||||
|                 block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'; | ||||
|             } | ||||
|           } | ||||
|           return of(blocks as AccelerationBlock[]); | ||||
|         }), | ||||
|         tap(() => { | ||||
|           this.loadingBlocks = false; | ||||
|         }) | ||||
|       ) | ||||
|     ]).pipe( | ||||
|       switchMap(([accelerations, blocks]) => { | ||||
|         const blockMap = {}; | ||||
|         for (const block of blocks) { | ||||
|           blockMap[block.id] = block; | ||||
|         } | ||||
|         const accelerationsByBlock: { [ hash: string ]: Acceleration[] } = {}; | ||||
|         for (const acceleration of accelerations) { | ||||
|           if (['mined', 'completed'].includes(acceleration.status) && acceleration.pools.includes(blockMap[acceleration.blockHash]?.extras.pool.id)) { | ||||
|             if (!accelerationsByBlock[acceleration.blockHash]) { | ||||
|               accelerationsByBlock[acceleration.blockHash] = []; | ||||
|             } | ||||
|             accelerationsByBlock[acceleration.blockHash].push(acceleration); | ||||
|           } | ||||
|         } | ||||
|         return of(blocks.slice(0, 6).map(block => { | ||||
|           block.accelerationCount = (accelerationsByBlock[block.id] || []).length; | ||||
|           return block; | ||||
|         })); | ||||
|       }) | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   getAcceleratorColor(tx: TxView): Color { | ||||
|     if (tx.status === 'accelerated' || tx.acc) { | ||||
|       return acceleratedColor; | ||||
|     } else { | ||||
|       const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate
 | ||||
|       const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1; | ||||
|       return normalColors[feeLevelIndex] || normalColors[mempoolFeeColors.length - 1]; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,53 @@ | ||||
| <div class="stats-wrapper" *ngIf="accelerationStats$ | async as stats; else loading"> | ||||
|   <div class="stats-container"> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="address.transactions">Transactions</h5> | ||||
|       <div class="card-text"> | ||||
|         <div>{{ stats.count }}</div> | ||||
|         <div class="symbol" i18n="accelerator.total-accelerated">accelerated</div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="accelerator.average-max-bid">Avg Max Bid</h5> | ||||
|       <div class="card-text"> | ||||
|         <div>{{ stats.avgFeeDelta / 100_000_000 | amountShortener: 4 }} <span class="symbol" i18n="shared.btc|BTC">BTC</span></div> | ||||
|         <span class="fiat"> | ||||
|           <app-fiat [value]="stats.avgFeeDelta"></app-fiat> | ||||
|         </span> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="accelerator.total-vsize">Total vsize</h5> | ||||
|       <div class="card-text"> | ||||
|         <div [innerHTML]="'‎' + (stats.totalVsize * 4 | vbytes: 2)"></div> | ||||
|         <div class="symbol">{{ (stats.totalVsize / 1_000_000 * 100).toFixed(2) }}% <span i18n="accelerator.percent-of-next-block"> of next block</span></div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
| <ng-template #loading> | ||||
|   <div class="stats-container loading-container"> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="address.transactions">Transactions</h5> | ||||
|       <div class="card-text"> | ||||
|         <div class="skeleton-loader"></div> | ||||
|         <div class="skeleton-loader"></div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="accelerator.average-max-bid">Avg Max Bid</h5> | ||||
|       <div class="card-text"> | ||||
|         <div class="skeleton-loader"></div> | ||||
|         <div class="skeleton-loader"></div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="accelerator.total-vsize">Total vsize</h5> | ||||
|       <div class="card-text"> | ||||
|         <div class="skeleton-loader"></div> | ||||
|         <div class="skeleton-loader"></div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </ng-template> | ||||
| @ -0,0 +1,88 @@ | ||||
| .card-title { | ||||
|   color: #4a68b9; | ||||
|   font-size: 10px; | ||||
|   margin-bottom: 4px;   | ||||
|   font-size: 1rem; | ||||
|   overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
|   white-space: nowrap; | ||||
| } | ||||
| 
 | ||||
| .card-text { | ||||
|   font-size: 22px; | ||||
|   span { | ||||
|     font-size: 11px; | ||||
|     position: relative; | ||||
|     top: -2px; | ||||
|     display: inline-flex; | ||||
|   } | ||||
|   .green-color { | ||||
|     display: block; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .stats-container { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   @media (min-width: 376px) { | ||||
|     flex-direction: row; | ||||
|   }   | ||||
|   .item { | ||||
|     max-width: 150px; | ||||
|     margin: 0; | ||||
|     width: -webkit-fill-available; | ||||
|     @media (min-width: 376px) { | ||||
|       margin: 0 auto 0px; | ||||
|     }     | ||||
|     &:first-child{ | ||||
|       display: none; | ||||
|       @media (min-width: 485px) { | ||||
|         display: block; | ||||
|       }     | ||||
|       @media (min-width: 768px) { | ||||
|         display: none; | ||||
|       }     | ||||
|       @media (min-width: 992px) { | ||||
|         display: block; | ||||
|       }     | ||||
|     } | ||||
|     &:last-child { | ||||
|       margin-bottom: 0; | ||||
|     } | ||||
|     .card-text span { | ||||
|       color: #ffffff66; | ||||
|       font-size: 12px; | ||||
|       top: 0px; | ||||
|     } | ||||
|     .fee-text{ | ||||
|       border-bottom: 1px solid #ffffff1c; | ||||
|       width: fit-content; | ||||
|       margin: auto; | ||||
|       line-height: 1.45; | ||||
|       padding: 0px 2px; | ||||
|     } | ||||
|     .fiat { | ||||
|       display: block; | ||||
|       font-size: 14px !important; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .loading-container{ | ||||
|   min-height: 76px; | ||||
| } | ||||
| 
 | ||||
| .card-text { | ||||
|   .skeleton-loader { | ||||
|     width: 100%; | ||||
|     display: block; | ||||
|     &:first-child { | ||||
|       max-width: 90px; | ||||
|       margin: 15px auto 3px; | ||||
|     } | ||||
|     &:last-child { | ||||
|       margin: 10px auto 3px; | ||||
|       max-width: 55px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,41 @@ | ||||
| import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; | ||||
| import { Observable, of } from 'rxjs'; | ||||
| import { switchMap } from 'rxjs/operators'; | ||||
| import { ApiService } from '../../../services/api.service'; | ||||
| import { Acceleration } from '../../../interfaces/node-api.interface'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-pending-stats', | ||||
|   templateUrl: './pending-stats.component.html', | ||||
|   styleUrls: ['./pending-stats.component.scss'], | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class PendingStatsComponent implements OnInit { | ||||
|   @Input() accelerations$: Observable<Acceleration[]>; | ||||
|   public accelerationStats$: Observable<any>; | ||||
| 
 | ||||
|   constructor( | ||||
|     private apiService: ApiService, | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.accelerationStats$ = (this.accelerations$ || this.apiService.getAccelerations$()).pipe( | ||||
|       switchMap(accelerations => { | ||||
|         let totalAccelerations = 0; | ||||
|         let totalFeeDelta = 0; | ||||
|         let totalVsize = 0; | ||||
|         for (const acceleration of accelerations) { | ||||
|           totalAccelerations++; | ||||
|           totalFeeDelta += acceleration.feeDelta || 0; | ||||
|           totalVsize += acceleration.effectiveVsize || 0; | ||||
|         } | ||||
|         return of({ | ||||
|           count: totalAccelerations, | ||||
|           totalFeeDelta, | ||||
|           avgFeeDelta: totalAccelerations ? totalFeeDelta / totalAccelerations : 0, | ||||
|           totalVsize, | ||||
|         }); | ||||
|       }) | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @ -1,7 +1,7 @@ | ||||
| <app-indexing-progress></app-indexing-progress> | ||||
| <app-indexing-progress *ngIf="!widget"></app-indexing-progress> | ||||
| 
 | ||||
| <div class="full-container"> | ||||
|   <div class="card-header mb-0 mb-md-4"> | ||||
| <div [class.full-container]="!widget"> | ||||
|   <div *ngIf="!widget" class="card-header mb-0 mb-md-4"> | ||||
|     <div class="d-flex d-md-block align-items-baseline"> | ||||
|       <span i18n="mining.block-fee-rates">Block Fee Rates</span> | ||||
|       <button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()"> | ||||
| @ -45,7 +45,24 @@ | ||||
|     </form> | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions" | ||||
|   <div *ngIf="widget"> | ||||
|     <div class="block-fee-rates"> | ||||
|       <div class="item" *ngIf="(hrStatsObservable$ | async) as stats; else loadingHrStats"> | ||||
|         <h5 class="card-title" i18n="mining.avg-block-fee-24h">Avg Block Fee (24h)</h5> | ||||
|         <p class="card-text"> | ||||
|           <app-fee-rate [fee]="stats.avgMedianRate"></app-fee-rate> | ||||
|         </p> | ||||
|       </div> | ||||
|       <div class="item" *ngIf="(statsObservable$ | async) as stats; else loadingStats"> | ||||
|         <h5 class="card-title" i18n="mining.avg-block-fee-1m">Avg Block Fee (1m)</h5> | ||||
|         <p class="card-text"> | ||||
|           <app-fee-rate [fee]="stats.avgMedianRate"></app-fee-rate> | ||||
|         </p> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <div [class.chart]="!widget" [class.chart-widget]="widget" echarts [initOpts]="chartInitOptions" [options]="chartOptions" | ||||
|     (chartInit)="onChartInit($event)"> | ||||
|   </div> | ||||
|   <div class="text-center loadingGraphs" *ngIf="isLoading"> | ||||
| @ -53,3 +70,20 @@ | ||||
|   </div> | ||||
| 
 | ||||
| </div> | ||||
| 
 | ||||
| <ng-template #loadingHrStats> | ||||
|   <div class="item"> | ||||
|     <h5 class="card-title" i18n="mining.avg-block-fee-24h">Avg Block Fee (24h)</h5> | ||||
|     <p class="card-text"> | ||||
|       <span class="skeleton-loader skeleton-loader-big"></span> | ||||
|     </p> | ||||
|   </div> | ||||
| </ng-template> | ||||
| <ng-template #loadingStats> | ||||
|   <div class="item"> | ||||
|     <h5 class="card-title" i18n="mining.avg-block-fee-1m">Avg Block Fee (1m)</h5> | ||||
|     <p class="card-text"> | ||||
|       <span class="skeleton-loader skeleton-loader-big"></span> | ||||
|     </p> | ||||
|   </div> | ||||
| </ng-template> | ||||
| @ -57,7 +57,54 @@ | ||||
| .chart-widget { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   max-height: 270px; | ||||
|   max-height: 238px; | ||||
| } | ||||
| 
 | ||||
| .block-fee-rates { | ||||
|   min-height: 56px; | ||||
|   display: block; | ||||
|   @media (min-width: 485px) { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|   } | ||||
|   h5 { | ||||
|     margin-bottom: 10px; | ||||
|   } | ||||
|   .item { | ||||
|     width: 50%; | ||||
|     display: inline-block; | ||||
|     margin: 0px auto 20px; | ||||
|     &:nth-child(2) { | ||||
|       order: 2; | ||||
|       @media (min-width: 485px) { | ||||
|         order: 3; | ||||
|       } | ||||
|     } | ||||
|     &:nth-child(3) { | ||||
|       order: 3; | ||||
|       @media (min-width: 485px) { | ||||
|         order: 2; | ||||
|         display: block; | ||||
|       } | ||||
|       @media (min-width: 768px) { | ||||
|         display: none; | ||||
|       } | ||||
|       @media (min-width: 992px) { | ||||
|         display: block; | ||||
|       } | ||||
|     } | ||||
|     .card-title { | ||||
|       font-size: 1rem; | ||||
|       color: #4a68b9; | ||||
|     } | ||||
|     .card-text { | ||||
|       font-size: 18px; | ||||
|       span { | ||||
|         color: #ffffff66; | ||||
|         font-size: 12px; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .formRadioGroup { | ||||
| @ -85,6 +132,13 @@ | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .skeleton-loader { | ||||
|   width: 100%; | ||||
|   display: block; | ||||
|   max-width: 80px; | ||||
|   margin: 15px auto 3px; | ||||
| } | ||||
| 
 | ||||
| .disabled { | ||||
|   pointer-events: none; | ||||
|   opacity: 0.5; | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core'; | ||||
| import { EChartsOption } from '../../graphs/echarts'; | ||||
| import { Observable, Subscription, combineLatest } from 'rxjs'; | ||||
| import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core'; | ||||
| import { EChartsOption, graphic } from 'echarts'; | ||||
| import { Observable, combineLatest, of } from 'rxjs'; | ||||
| import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; | ||||
| import { ApiService } from '../../services/api.service'; | ||||
| import { SeoService } from '../../services/seo.service'; | ||||
| @ -29,6 +29,7 @@ import { ActivatedRoute, Router } from '@angular/router'; | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class BlockFeeRatesGraphComponent implements OnInit { | ||||
|   @Input() widget = false; | ||||
|   @Input() right: number | string = 45; | ||||
|   @Input() left: number | string = 75; | ||||
| 
 | ||||
| @ -40,6 +41,7 @@ export class BlockFeeRatesGraphComponent implements OnInit { | ||||
|     renderer: 'svg', | ||||
|   }; | ||||
| 
 | ||||
|   hrStatsObservable$: Observable<any>; | ||||
|   statsObservable$: Observable<any>; | ||||
|   isLoading = true; | ||||
|   formatNumber = formatNumber; | ||||
| @ -57,18 +59,24 @@ export class BlockFeeRatesGraphComponent implements OnInit { | ||||
|     private router: Router, | ||||
|     private zone: NgZone, | ||||
|     private route: ActivatedRoute, | ||||
|     private cd: ChangeDetectorRef, | ||||
|   ) { | ||||
|     this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' }); | ||||
|     this.radioGroupForm.controls.dateSpan.setValue('1y'); | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     if (this.widget) { | ||||
|       this.miningWindowPreference = '1m'; | ||||
|     } else { | ||||
|       this.seoService.setTitle($localize`:@@ed8e33059967f554ff06b4f5b6049c465b92d9b3:Block Fee Rates`); | ||||
|       this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fee-rates:See Bitcoin feerates visualized over time, including minimum and maximum feerates per block along with feerates at various percentiles.`); | ||||
|       this.miningWindowPreference = this.miningService.getDefaultTimespan('24h'); | ||||
|     } | ||||
|     this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); | ||||
|     this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); | ||||
| 
 | ||||
|     if (!this.widget) { | ||||
|       this.route | ||||
|         .fragment | ||||
|         .subscribe((fragment) => { | ||||
| @ -76,20 +84,36 @@ export class BlockFeeRatesGraphComponent implements OnInit { | ||||
|             this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false }); | ||||
|           } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     this.hrStatsObservable$ = combineLatest([ | ||||
|       this.apiService.getHistoricalBlockFeeRates$('24h'), | ||||
|       this.stateService.rateUnits$ | ||||
|     ]).pipe( | ||||
|       map(([response, rateUnits]) => { | ||||
|         return { | ||||
|           blockCount: parseInt(response.headers.get('x-total-count'), 10), | ||||
|           avgMedianRate: response.body.length ? response.body.reduce((acc, rate) => acc + rate.avgFee_50, 0) / response.body.length : 0, | ||||
|         }; | ||||
|       }), | ||||
|       share(), | ||||
|     ); | ||||
| 
 | ||||
|     this.statsObservable$ = combineLatest([ | ||||
|         this.radioGroupForm.get('dateSpan').valueChanges.pipe(startWith(this.radioGroupForm.controls.dateSpan.value)), | ||||
|         this.widget ? of(this.miningWindowPreference) : this.radioGroupForm.get('dateSpan').valueChanges.pipe(startWith(this.radioGroupForm.controls.dateSpan.value)), | ||||
|         this.stateService.rateUnits$ | ||||
|     ]).pipe( | ||||
|         switchMap(([timespan, rateUnits]) => { | ||||
|           if (!this.widget) { | ||||
|             this.storageService.setValue('miningWindowPreference', timespan); | ||||
|           } | ||||
|           this.timespan = timespan; | ||||
|           this.isLoading = true; | ||||
|           return this.apiService.getHistoricalBlockFeeRates$(timespan) | ||||
|             .pipe( | ||||
|               tap((response) => { | ||||
|                 // Group by percentile
 | ||||
|                 const seriesData = { | ||||
|                 const seriesData = this.widget ? { 'Median': [] } : { | ||||
|                   'Min': [], | ||||
|                   '10th': [], | ||||
|                   '25th': [], | ||||
| @ -100,6 +124,9 @@ export class BlockFeeRatesGraphComponent implements OnInit { | ||||
|                 }; | ||||
|                 for (const rate of response.body) { | ||||
|                   const timestamp = rate.timestamp * 1000; | ||||
|                   if (this.widget) { | ||||
|                     seriesData['Median'].push([timestamp, rate.avgFee_50, rate.avgHeight]); | ||||
|                   } else { | ||||
|                     seriesData['Min'].push([timestamp, rate.avgFee_0, rate.avgHeight]); | ||||
|                     seriesData['10th'].push([timestamp, rate.avgFee_10, rate.avgHeight]); | ||||
|                     seriesData['25th'].push([timestamp, rate.avgFee_25, rate.avgHeight]); | ||||
| @ -108,6 +135,7 @@ export class BlockFeeRatesGraphComponent implements OnInit { | ||||
|                     seriesData['90th'].push([timestamp, rate.avgFee_90, rate.avgHeight]); | ||||
|                     seriesData['Max'].push([timestamp, rate.avgFee_100, rate.avgHeight]); | ||||
|                   } | ||||
|                 } | ||||
| 
 | ||||
|                 // Prepare chart
 | ||||
|                 const series = []; | ||||
| @ -135,15 +163,42 @@ export class BlockFeeRatesGraphComponent implements OnInit { | ||||
|                   }); | ||||
|                 } | ||||
| 
 | ||||
|                 if (this.widget) { | ||||
|                   let maResolution = 30; | ||||
|                   const medianMa = []; | ||||
|                   for (let i = maResolution - 1; i < seriesData['Median'].length; ++i) { | ||||
|                     let avg = 0; | ||||
|                     for (let y = maResolution - 1; y >= 0; --y) { | ||||
|                       avg += seriesData['Median'][i - y][1]; | ||||
|                     } | ||||
|                     avg /= maResolution; | ||||
|                     medianMa.push([seriesData['Median'][i][0], avg, seriesData['Median'][i][2]]); | ||||
|                   } | ||||
|                   series.push({ | ||||
|                     zlevel: 1, | ||||
|                     name: 'Moving average', | ||||
|                     data: medianMa, | ||||
|                     type: 'line', | ||||
|                     showSymbol: false, | ||||
|                     symbol: 'none', | ||||
|                     lineStyle: { | ||||
|                       width: 3, | ||||
|                     } | ||||
|                   }); | ||||
|                 } | ||||
| 
 | ||||
|                 this.prepareChartOptions({ | ||||
|                   legends: legends, | ||||
|                   series: series | ||||
|                 }, rateUnits === 'wu'); | ||||
| 
 | ||||
|                 this.isLoading = false; | ||||
|                 this.cd.markForCheck(); | ||||
|               }), | ||||
|               map((response) => { | ||||
|                 return { | ||||
|                   blockCount: parseInt(response.headers.get('x-total-count'), 10), | ||||
|                   avgMedianRate: response.body.length ? response.body.reduce((acc, rate) => acc + rate.avgFee_50, 0) / response.body.length : 0, | ||||
|                 }; | ||||
|               }), | ||||
|             ); | ||||
| @ -154,13 +209,19 @@ export class BlockFeeRatesGraphComponent implements OnInit { | ||||
| 
 | ||||
|   prepareChartOptions(data, weightMode) { | ||||
|     this.chartOptions = { | ||||
|       color: ['#D81B60', '#8E24AA', '#1E88E5', '#7CB342', '#FDD835', '#6D4C41', '#546E7A'], | ||||
|       color: this.widget ? ['#6b6b6b', 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', '#8E24AA', '#1E88E5', '#7CB342', '#FDD835', '#6D4C41', '#546E7A'], | ||||
|       animation: false, | ||||
|       grid: { | ||||
|         right: this.right, | ||||
|         left: this.left, | ||||
|         bottom: 80, | ||||
|         top: this.isMobile() ? 10 : 50, | ||||
|         bottom: this.widget ? 30 : 80, | ||||
|         top: this.widget ? 20 : (this.isMobile() ? 10 : 50), | ||||
|       }, | ||||
|       tooltip: { | ||||
|         show: !this.isMobile(), | ||||
| @ -184,9 +245,9 @@ export class BlockFeeRatesGraphComponent implements OnInit { | ||||
| 
 | ||||
|           for (const rate of data.reverse()) { | ||||
|             if (weightMode) { | ||||
|               tooltip += `${rate.marker} ${rate.seriesName}: ${rate.data[1] / 4} sats/WU<br>`; | ||||
|               tooltip += `${rate.marker} ${rate.seriesName}: ${(rate.data[1] / 4).toFixed(2)} sats/WU<br>`; | ||||
|             } else { | ||||
|               tooltip += `${rate.marker} ${rate.seriesName}: ${rate.data[1]} sats/vByte<br>`; | ||||
|               tooltip += `${rate.marker} ${rate.seriesName}: ${rate.data[1].toFixed(2)} sats/vByte<br>`; | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
| @ -201,7 +262,7 @@ export class BlockFeeRatesGraphComponent implements OnInit { | ||||
|       }, | ||||
|       xAxis: data.series.length === 0 ? undefined : | ||||
|       { | ||||
|         name: formatterXAxisLabel(this.locale, this.timespan), | ||||
|         name: this.widget ? undefined : formatterXAxisLabel(this.locale, this.timespan), | ||||
|         nameLocation: 'middle', | ||||
|         nameTextStyle: { | ||||
|           padding: [10, 0, 0, 0], | ||||
| @ -218,7 +279,7 @@ export class BlockFeeRatesGraphComponent implements OnInit { | ||||
|           padding: [0, 5], | ||||
|         }, | ||||
|       }, | ||||
|       legend: (data.series.length === 0) ? undefined : { | ||||
|       legend: (this.widget || data.series.length === 0) ? undefined : { | ||||
|         padding: [10, 75], | ||||
|         data: data.legends, | ||||
|         selected: JSON.parse(this.storageService.getValue('fee_rates_legend')) ?? { | ||||
| @ -256,7 +317,7 @@ export class BlockFeeRatesGraphComponent implements OnInit { | ||||
|         max: (val) => this.timespan === 'all' ? Math.min(val.max, 5000) : undefined, | ||||
|       }, | ||||
|       series: data.series, | ||||
|       dataZoom: [{ | ||||
|       dataZoom: this.widget ? null : [{ | ||||
|         type: 'inside', | ||||
|         realtime: true, | ||||
|         zoomLock: true, | ||||
|  | ||||
| @ -4,7 +4,7 @@ import { FastVertexArray } from './fast-vertex-array'; | ||||
| import BlockScene from './block-scene'; | ||||
| import TxSprite from './tx-sprite'; | ||||
| import TxView from './tx-view'; | ||||
| import { Position } from './sprite-types'; | ||||
| import { Color, Position } from './sprite-types'; | ||||
| import { Price } from '../../services/price.service'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| @ -27,6 +27,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | ||||
|   @Input() unavailable: boolean = false; | ||||
|   @Input() auditHighlighting: boolean = false; | ||||
|   @Input() blockConversion: Price; | ||||
|   @Input() overrideColors: ((tx: TxView) => Color) | null = null; | ||||
|   @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>(); | ||||
|   @Output() txHoverEvent = new EventEmitter<string>(); | ||||
|   @Output() readyEvent = new EventEmitter(); | ||||
| @ -91,6 +92,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | ||||
|     if (changes.auditHighlighting) { | ||||
|       this.setHighlightingEnabled(this.auditHighlighting); | ||||
|     } | ||||
|     if (changes.overrideColor) { | ||||
|       this.scene.setColorFunction(this.overrideColors); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
| @ -228,7 +232,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | ||||
|     } else { | ||||
|       this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution, | ||||
|         blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, | ||||
|         highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset }); | ||||
|         highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset, | ||||
|         colorFunction: this.overrideColors }); | ||||
|       this.start(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -1,12 +1,26 @@ | ||||
| import { FastVertexArray } from './fast-vertex-array'; | ||||
| import TxView from './tx-view'; | ||||
| import { TransactionStripped } from '../../interfaces/websocket.interface'; | ||||
| import { Position, Square, ViewUpdateParams } from './sprite-types'; | ||||
| import { Color, Position, Square, ViewUpdateParams } from './sprite-types'; | ||||
| import { feeLevels, mempoolFeeColors } from '../../app.constants'; | ||||
| import { darken, desaturate, hexToColor } from './utils'; | ||||
| 
 | ||||
| const feeColors = mempoolFeeColors.map(hexToColor); | ||||
| const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9)); | ||||
| const marginalFeeColors = feeColors.map((color) => darken(desaturate(color, 0.8), 1.1)); | ||||
| const auditColors = { | ||||
|   censored: hexToColor('f344df'), | ||||
|   missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), | ||||
|   added: hexToColor('0099ff'), | ||||
|   selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7), | ||||
|   accelerated: hexToColor('8F5FF6'), | ||||
| }; | ||||
| 
 | ||||
| export default class BlockScene { | ||||
|   scene: { count: number, offset: { x: number, y: number}}; | ||||
|   vertexArray: FastVertexArray; | ||||
|   txs: { [key: string]: TxView }; | ||||
|   getColor: ((tx: TxView) => Color) = defaultColorFunction; | ||||
|   orientation: string; | ||||
|   flip: boolean; | ||||
|   animationDuration: number = 1000; | ||||
| @ -26,11 +40,11 @@ export default class BlockScene { | ||||
|   animateUntil = 0; | ||||
|   dirty: boolean; | ||||
| 
 | ||||
|   constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }: | ||||
|   constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting, colorFunction }: | ||||
|       { width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, | ||||
|         orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean } | ||||
|         orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null } | ||||
|   ) { | ||||
|     this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }); | ||||
|     this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting, colorFunction }); | ||||
|   } | ||||
| 
 | ||||
|   resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void { | ||||
| @ -63,6 +77,14 @@ export default class BlockScene { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void { | ||||
|     this.getColor = colorFunction; | ||||
|     this.dirty = true; | ||||
|     if (this.initialised && this.scene) { | ||||
|       this.updateColors(performance.now(), 50); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Destroy the current layout and clean up graphics sprites without any exit animation
 | ||||
|   destroy(): void { | ||||
|     Object.values(this.txs).forEach(tx => tx.destroy()); | ||||
| @ -86,7 +108,7 @@ export default class BlockScene { | ||||
|       this.applyTxUpdate(txView, { | ||||
|         display: { | ||||
|           position: txView.screenPosition, | ||||
|           color: txView.getColor() | ||||
|           color: this.getColor(txView) | ||||
|         }, | ||||
|         duration: 0 | ||||
|       }); | ||||
| @ -217,9 +239,9 @@ export default class BlockScene { | ||||
|     this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value)); | ||||
|   } | ||||
| 
 | ||||
|   private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }: | ||||
|   private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting, colorFunction }: | ||||
|       { width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, | ||||
|         orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean } | ||||
|         orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null } | ||||
|   ): void { | ||||
|     this.animationDuration = animationDuration || 1000; | ||||
|     this.configAnimationOffset = animationOffset; | ||||
| @ -228,6 +250,7 @@ export default class BlockScene { | ||||
|     this.flip = flip; | ||||
|     this.vertexArray = vertexArray; | ||||
|     this.highlightingEnabled = highlighting; | ||||
|     this.getColor = colorFunction || defaultColorFunction; | ||||
| 
 | ||||
|     this.scene = { | ||||
|       count: 0, | ||||
| @ -261,9 +284,23 @@ export default class BlockScene { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private updateColor(tx: TxView, startTime: number, delay: number, animate: boolean = true, duration: number = 500): void { | ||||
|     if (tx.dirty || this.dirty) { | ||||
|       const txColor = this.getColor(tx); | ||||
|       this.applyTxUpdate(tx, { | ||||
|         display: { | ||||
|           color: txColor, | ||||
|         }, | ||||
|         start: startTime, | ||||
|         delay, | ||||
|         duration: animate ? duration : 0, | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private setTxOnScreen(tx: TxView, startTime: number, delay: number = 50, direction: string = 'left', animate: boolean = true): void { | ||||
|     if (!tx.initialised) { | ||||
|       const txColor = tx.getColor(); | ||||
|       const txColor = this.getColor(tx); | ||||
|       this.applyTxUpdate(tx, { | ||||
|         display: { | ||||
|           position: { | ||||
| @ -321,6 +358,15 @@ export default class BlockScene { | ||||
|     this.dirty = false; | ||||
|   } | ||||
| 
 | ||||
|   private updateColors(startTime: number, delay: number = 50, animate: boolean = true, duration: number = 500): void { | ||||
|     const ids = this.getTxList(); | ||||
|     startTime = startTime || performance.now(); | ||||
|     for (const id of ids) { | ||||
|       this.updateColor(this.txs[id], startTime, delay, animate, duration); | ||||
|     } | ||||
|     this.dirty = false; | ||||
|   } | ||||
| 
 | ||||
|   private remove(id: string, startTime: number, direction: string = 'left'): TxView | void { | ||||
|     const tx = this.txs[id]; | ||||
|     if (tx) { | ||||
| @ -858,3 +904,48 @@ class BlockLayout { | ||||
| function feeRateDescending(a: TxView, b: TxView) { | ||||
|   return b.feerate - a.feerate; | ||||
| } | ||||
| 
 | ||||
| function defaultColorFunction(tx: TxView): Color { | ||||
|   const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate
 | ||||
|   const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1; | ||||
|   const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1]; | ||||
|   // Normal mode
 | ||||
|   if (!tx.scene?.highlightingEnabled) { | ||||
|     if (tx.acc) { | ||||
|       return auditColors.accelerated; | ||||
|     } else { | ||||
|       return feeLevelColor; | ||||
|     } | ||||
|     return feeLevelColor; | ||||
|   } | ||||
|   // Block audit
 | ||||
|   switch(tx.status) { | ||||
|     case 'censored': | ||||
|       return auditColors.censored; | ||||
|     case 'missing': | ||||
|     case 'sigop': | ||||
|     case 'rbf': | ||||
|       return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; | ||||
|     case 'fresh': | ||||
|     case 'freshcpfp': | ||||
|       return auditColors.missing; | ||||
|     case 'added': | ||||
|       return auditColors.added; | ||||
|     case 'selected': | ||||
|       return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; | ||||
|     case 'accelerated': | ||||
|       return auditColors.accelerated; | ||||
|     case 'found': | ||||
|       if (tx.context === 'projected') { | ||||
|         return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1]; | ||||
|       } else { | ||||
|         return feeLevelColor; | ||||
|       } | ||||
|     default: | ||||
|       if (tx.acc) { | ||||
|         return auditColors.accelerated; | ||||
|       } else { | ||||
|         return feeLevelColor; | ||||
|       } | ||||
|   } | ||||
| } | ||||
| @ -2,24 +2,13 @@ import TxSprite from './tx-sprite'; | ||||
| import { FastVertexArray } from './fast-vertex-array'; | ||||
| import { TransactionStripped } from '../../interfaces/websocket.interface'; | ||||
| import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types'; | ||||
| import { feeLevels, mempoolFeeColors } from '../../app.constants'; | ||||
| import { hexToColor } from './utils'; | ||||
| import BlockScene from './block-scene'; | ||||
| 
 | ||||
| const hoverTransitionTime = 300; | ||||
| const defaultHoverColor = hexToColor('1bd8f4'); | ||||
| const defaultHighlightColor = hexToColor('800080'); | ||||
| 
 | ||||
| const feeColors = mempoolFeeColors.map(hexToColor); | ||||
| const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9)); | ||||
| const marginalFeeColors = feeColors.map((color) => darken(desaturate(color, 0.8), 1.1)); | ||||
| const auditColors = { | ||||
|   censored: hexToColor('f344df'), | ||||
|   missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), | ||||
|   added: hexToColor('0099ff'), | ||||
|   selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7), | ||||
|   accelerated: hexToColor('8F5FF6'), | ||||
| }; | ||||
| 
 | ||||
| // convert from this class's update format to TxSprite's update format
 | ||||
| function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams { | ||||
|   return { | ||||
| @ -195,77 +184,4 @@ export default class TxView implements TransactionStripped { | ||||
|     this.dirty = false; | ||||
|     return performance.now() + hoverTransitionTime; | ||||
|   } | ||||
| 
 | ||||
|   getColor(): Color { | ||||
|     const rate = this.fee / this.vsize; // color by simple single-tx fee rate
 | ||||
|     const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1; | ||||
|     const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1]; | ||||
|     // Normal mode
 | ||||
|     if (!this.scene?.highlightingEnabled) { | ||||
|       if (this.acc) { | ||||
|         return auditColors.accelerated; | ||||
|       } else { | ||||
|         return feeLevelColor; | ||||
|       } | ||||
|       return feeLevelColor; | ||||
|     } | ||||
|     // Block audit
 | ||||
|     switch(this.status) { | ||||
|       case 'censored': | ||||
|         return auditColors.censored; | ||||
|       case 'missing': | ||||
|       case 'sigop': | ||||
|       case 'rbf': | ||||
|         return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; | ||||
|       case 'fresh': | ||||
|       case 'freshcpfp': | ||||
|         return auditColors.missing; | ||||
|       case 'added': | ||||
|         return auditColors.added; | ||||
|       case 'selected': | ||||
|         return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; | ||||
|       case 'accelerated': | ||||
|         return auditColors.accelerated; | ||||
|       case 'found': | ||||
|         if (this.context === 'projected') { | ||||
|           return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1]; | ||||
|         } else { | ||||
|           return feeLevelColor; | ||||
|         } | ||||
|       default: | ||||
|         if (this.acc) { | ||||
|           return auditColors.accelerated; | ||||
|         } else { | ||||
|           return feeLevelColor; | ||||
|         } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function hexToColor(hex: string): Color { | ||||
|   return { | ||||
|     r: parseInt(hex.slice(0, 2), 16) / 255, | ||||
|     g: parseInt(hex.slice(2, 4), 16) / 255, | ||||
|     b: parseInt(hex.slice(4, 6), 16) / 255, | ||||
|     a: 1 | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function desaturate(color: Color, amount: number): Color { | ||||
|   const gray = (color.r + color.g + color.b) / 6; | ||||
|   return { | ||||
|     r: color.r + ((gray - color.r) * amount), | ||||
|     g: color.g + ((gray - color.g) * amount), | ||||
|     b: color.b + ((gray - color.b) * amount), | ||||
|     a: color.a, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function darken(color: Color, amount: number): Color { | ||||
|   return { | ||||
|     r: color.r * amount, | ||||
|     g: color.g * amount, | ||||
|     b: color.b * amount, | ||||
|     a: color.a, | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										29
									
								
								frontend/src/app/components/block-overview-graph/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/src/app/components/block-overview-graph/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | ||||
| import { Color } from './sprite-types'; | ||||
| 
 | ||||
| export function hexToColor(hex: string): Color { | ||||
|   return { | ||||
|     r: parseInt(hex.slice(0, 2), 16) / 255, | ||||
|     g: parseInt(hex.slice(2, 4), 16) / 255, | ||||
|     b: parseInt(hex.slice(4, 6), 16) / 255, | ||||
|     a: hex.length > 6 ? parseInt(hex.slice(6, 8), 16) / 255 : 1 | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export function desaturate(color: Color, amount: number): Color { | ||||
|   const gray = (color.r + color.g + color.b) / 6; | ||||
|   return { | ||||
|     r: color.r + ((gray - color.r) * amount), | ||||
|     g: color.g + ((gray - color.g) * amount), | ||||
|     b: color.b + ((gray - color.b) * amount), | ||||
|     a: color.a, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export function darken(color: Color, amount: number): Color { | ||||
|   return { | ||||
|     r: color.r * amount, | ||||
|     g: color.g * amount, | ||||
|     b: color.b * amount, | ||||
|     a: color.a, | ||||
|   } | ||||
| } | ||||
| @ -79,7 +79,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { | ||||
|   } | ||||
| 
 | ||||
|   enabledMiningInfoIfNeeded(url) { | ||||
|     this.showMiningInfo = url.indexOf('/mining') !== -1; | ||||
|     this.showMiningInfo = url.includes('/mining') || url.includes('/acceleration'); | ||||
|     this.cd.markForCheck(); // Need to update the view asap
 | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -5,5 +5,6 @@ | ||||
|   [blockLimit]="stateService.blockVSize" | ||||
|   [orientation]="timeLtr ? 'right' : 'left'" | ||||
|   [flip]="true" | ||||
|   [overrideColors]="overrideColors" | ||||
|   (txClickEvent)="onTxClick($event)" | ||||
| ></app-block-overview-graph> | ||||
|  | ||||
| @ -8,6 +8,8 @@ import { switchMap, filter } from 'rxjs/operators'; | ||||
| import { WebsocketService } from '../../services/websocket.service'; | ||||
| import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; | ||||
| import { Router } from '@angular/router'; | ||||
| import { Color } from '../block-overview-graph/sprite-types'; | ||||
| import TxView from '../block-overview-graph/tx-view'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-mempool-block-overview', | ||||
| @ -16,6 +18,7 @@ import { Router } from '@angular/router'; | ||||
| }) | ||||
| export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit { | ||||
|   @Input() index: number; | ||||
|   @Input() overrideColors: ((tx: TxView) => Color) | null = null; | ||||
|   @Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>(); | ||||
| 
 | ||||
|   @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent; | ||||
|  | ||||
| @ -90,7 +90,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { | ||||
|   ) { } | ||||
| 
 | ||||
|   enabledMiningInfoIfNeeded(url) { | ||||
|     this.showMiningInfo = url.indexOf('/mining') !== -1; | ||||
|     this.showMiningInfo = url.includes('/mining') || url.includes('/acceleration'); | ||||
|     this.cd.markForCheck(); // Need to update the view asap
 | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -3,6 +3,7 @@ import { NgxEchartsModule } from 'ngx-echarts'; | ||||
| import { GraphsRoutingModule } from './graphs.routing.module'; | ||||
| import { SharedModule } from '../shared/shared.module'; | ||||
| 
 | ||||
| import { AccelerationFeesGraphComponent } from '../components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component'; | ||||
| import { BlockFeesGraphComponent } from '../components/block-fees-graph/block-fees-graph.component'; | ||||
| import { BlockRewardsGraphComponent } from '../components/block-rewards-graph/block-rewards-graph.component'; | ||||
| import { BlockFeeRatesGraphComponent } from '../components/block-fee-rates-graph/block-fee-rates-graph.component'; | ||||
| @ -19,6 +20,7 @@ import { PoolComponent } from '../components/pool/pool.component'; | ||||
| import { TelevisionComponent } from '../components/television/television.component'; | ||||
| import { DashboardComponent } from '../dashboard/dashboard.component'; | ||||
| import { MiningDashboardComponent } from '../components/mining-dashboard/mining-dashboard.component'; | ||||
| import { AcceleratorDashboardComponent } from '../components/acceleration/accelerator-dashboard/accelerator-dashboard.component'; | ||||
| import { HashrateChartComponent } from '../components/hashrate-chart/hashrate-chart.component'; | ||||
| import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools/hashrate-chart-pools.component'; | ||||
| import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component'; | ||||
| @ -30,12 +32,14 @@ import { CommonModule } from '@angular/common'; | ||||
|     MempoolBlockComponent, | ||||
| 
 | ||||
|     MiningDashboardComponent, | ||||
|     AcceleratorDashboardComponent, | ||||
|     PoolComponent, | ||||
|     PoolRankingComponent, | ||||
|     TelevisionComponent, | ||||
| 
 | ||||
|     StatisticsComponent, | ||||
|     GraphsComponent, | ||||
|     AccelerationFeesGraphComponent, | ||||
|     BlockFeesGraphComponent, | ||||
|     BlockRewardsGraphComponent, | ||||
|     BlockFeeRatesGraphComponent, | ||||
|  | ||||
| @ -10,12 +10,15 @@ import { HashrateChartComponent } from '../components/hashrate-chart/hashrate-ch | ||||
| import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools/hashrate-chart-pools.component'; | ||||
| import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.component'; | ||||
| import { MiningDashboardComponent } from '../components/mining-dashboard/mining-dashboard.component'; | ||||
| import { AcceleratorDashboardComponent } from '../components/acceleration/accelerator-dashboard/accelerator-dashboard.component'; | ||||
| import { PoolRankingComponent } from '../components/pool-ranking/pool-ranking.component'; | ||||
| import { PoolComponent } from '../components/pool/pool.component'; | ||||
| import { StartComponent } from '../components/start/start.component'; | ||||
| import { StatisticsComponent } from '../components/statistics/statistics.component'; | ||||
| import { TelevisionComponent } from '../components/television/television.component'; | ||||
| import { DashboardComponent } from '../dashboard/dashboard.component'; | ||||
| import { AccelerationFeesGraphComponent } from '../components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component'; | ||||
| import { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|   { | ||||
| @ -37,6 +40,22 @@ const routes: Routes = [ | ||||
|           }, | ||||
|         ] | ||||
|       }, | ||||
|       { | ||||
|         path: 'acceleration', | ||||
|         data: { networks: ['bitcoin'] }, | ||||
|         component: StartComponent, | ||||
|         children: [ | ||||
|           { | ||||
|             path: '', | ||||
|             component: AcceleratorDashboardComponent, | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       { | ||||
|         path: 'acceleration-list', | ||||
|         data: { networks: ['bitcoin'] }, | ||||
|         component: AccelerationsListComponent, | ||||
|       }, | ||||
|       { | ||||
|         path: 'mempool-block/:id', | ||||
|         data: { networks: ['bitcoin', 'liquid'] }, | ||||
| @ -93,6 +112,11 @@ const routes: Routes = [ | ||||
|             data: { networks: ['bitcoin'] }, | ||||
|             component: BlockSizesWeightsGraphComponent, | ||||
|           }, | ||||
|           { | ||||
|             path: 'acceleration/fees', | ||||
|             data: { networks: ['bitcoin'] }, | ||||
|             component: AccelerationFeesGraphComponent, | ||||
|           }, | ||||
|           { | ||||
|             path: 'lightning', | ||||
|             data: { preload: true, networks: ['bitcoin'] }, | ||||
|  | ||||
| @ -0,0 +1,8 @@ | ||||
| <span *ngIf="valueOverride !== undefined">{{ valueOverride }}</span> | ||||
| <span *ngIf="valueOverride === undefined">‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ value | number }} </span> | ||||
| <span class="symbol"> | ||||
|   <ng-template [ngIf]="network === 'liquid'">L-</ng-template> | ||||
|   <ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template> | ||||
|   <ng-template [ngIf]="network === 'testnet'">t-</ng-template> | ||||
|   <ng-template [ngIf]="network === 'signet'">s-</ng-template>{{ unit }} | ||||
| </span> | ||||
							
								
								
									
										44
									
								
								frontend/src/app/shared/components/btc/btc.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								frontend/src/app/shared/components/btc/btc.component.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | ||||
| import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| import { StateService } from '../../../services/state.service'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-btc', | ||||
|   templateUrl: './btc.component.html', | ||||
|   styleUrls: ['./btc.component.scss'] | ||||
| }) | ||||
| export class BtcComponent implements OnInit, OnChanges { | ||||
|   @Input() satoshis: number; | ||||
|   @Input() addPlus = false; | ||||
|   @Input() valueOverride: string | undefined = undefined; | ||||
| 
 | ||||
|   value: number; | ||||
|   unit: string; | ||||
| 
 | ||||
|   network = ''; | ||||
|   stateSubscription: Subscription; | ||||
| 
 | ||||
|   constructor( | ||||
|     private stateService: StateService, | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit() { | ||||
|     this.stateSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network); | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy() { | ||||
|     if (this.stateSubscription) { | ||||
|       this.stateSubscription.unsubscribe(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges(changes: SimpleChanges): void { | ||||
|     if (this.satoshis >= 1_000_000) { | ||||
|       this.value = (this.satoshis / 100_000_000); | ||||
|       this.unit = 'BTC' | ||||
|     } else { | ||||
|       this.value = Math.round(this.satoshis); | ||||
|       this.unit = 'sats' | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -73,6 +73,7 @@ import { IndexingProgressComponent } from '../components/indexing-progress/index | ||||
| import { SvgImagesComponent } from '../components/svg-images/svg-images.component'; | ||||
| import { ChangeComponent } from '../components/change/change.component'; | ||||
| import { SatsComponent } from './components/sats/sats.component'; | ||||
| import { BtcComponent } from './components/btc/btc.component'; | ||||
| import { FeeRateComponent } from './components/fee-rate/fee-rate.component'; | ||||
| import { TruncateComponent } from './components/truncate/truncate.component'; | ||||
| import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component'; | ||||
| @ -85,6 +86,9 @@ import { GlobalFooterComponent } from './components/global-footer/global-footer. | ||||
| import { AcceleratePreviewComponent } from '../components/accelerate-preview/accelerate-preview.component'; | ||||
| import { AccelerateFeeGraphComponent } from '../components/accelerate-preview/accelerate-fee-graph.component'; | ||||
| import { MempoolErrorComponent } from './components/mempool-error/mempool-error.component'; | ||||
| import { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component'; | ||||
| import { PendingStatsComponent } from '../components/acceleration/pending-stats/pending-stats.component'; | ||||
| import { AccelerationStatsComponent } from '../components/acceleration/acceleration-stats/acceleration-stats.component'; | ||||
| 
 | ||||
| import { BlockViewComponent } from '../components/block-view/block-view.component'; | ||||
| import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component'; | ||||
| @ -167,6 +171,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | ||||
|     SvgImagesComponent, | ||||
|     ChangeComponent, | ||||
|     SatsComponent, | ||||
|     BtcComponent, | ||||
|     FeeRateComponent, | ||||
|     TruncateComponent, | ||||
|     SearchResultsComponent, | ||||
| @ -190,6 +195,9 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | ||||
|     OnlyVsizeDirective, | ||||
|     OnlyWeightDirective, | ||||
|     MempoolErrorComponent, | ||||
|     AccelerationsListComponent, | ||||
|     AccelerationStatsComponent, | ||||
|     PendingStatsComponent, | ||||
|   ], | ||||
|   imports: [ | ||||
|     CommonModule, | ||||
| @ -287,6 +295,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | ||||
|     SvgImagesComponent, | ||||
|     ChangeComponent, | ||||
|     SatsComponent, | ||||
|     BtcComponent, | ||||
|     FeeRateComponent, | ||||
|     TruncateComponent, | ||||
|     SearchResultsComponent, | ||||
| @ -300,6 +309,9 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | ||||
|     AcceleratePreviewComponent, | ||||
|     AccelerateFeeGraphComponent, | ||||
|     MempoolErrorComponent, | ||||
|     AccelerationsListComponent, | ||||
|     AccelerationStatsComponent, | ||||
|     PendingStatsComponent, | ||||
| 
 | ||||
|     MempoolBlockOverviewComponent, | ||||
|     ClockchainComponent, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user