diff --git a/frontend/src/app/components/address-labels/address-labels.component.html b/frontend/src/app/components/address-labels/address-labels.component.html index 353e733ae..dfc6647f4 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.html +++ b/frontend/src/app/components/address-labels/address-labels.component.html @@ -1,9 +1,16 @@ - - {{ label }} - + + + + + {{ label }} + + + + + + + mining pool + + + + {{ poolStats.pool.name }} + + + + + + + + + + Tags + {{ poolStats.pool.regexes }} + + + Hashrate + {{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }} + + + + + + + + + + ~ + \ No newline at end of file diff --git a/frontend/src/app/components/pool/pool-preview.component.scss b/frontend/src/app/components/pool/pool-preview.component.scss new file mode 100644 index 000000000..533bac4af --- /dev/null +++ b/frontend/src/app/components/pool/pool-preview.component.scss @@ -0,0 +1,78 @@ +.stats { + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-start; + width: 100%; + max-width: 100%; + margin: 15px 0; + font-size: 32px; + overflow: hidden; + + .stat-box { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: baseline; + justify-content: space-between; + width: 100%; + margin-left: 15px; + background: #181b2d; + padding: 0.75rem; + width: 0; + flex-grow: 1; + + &:first-child { + margin-left: 0; + } + + .label { + flex-shrink: 0; + flex-grow: 0; + margin-right: 1em; + } + .data { + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } +} + +.chart { + width: 100%; + height: 315px; + background: #181b2d; +} + +.row { + margin-right: 0; +} + +.full-width-row { + padding-left: 15px; + flex-wrap: nowrap; +} + +.logo-wrapper { + position: relative; + width: 62px; + height: 62px; + margin-left: 1em; + + img { + position: absolute; + right: 0; + top: 0; + background: #24273e; + + &.noimg { + opacity: 0; + } + } +} + +::ng-deep .symbol { + font-size: 24px; +} diff --git a/frontend/src/app/components/pool/pool-preview.component.ts b/frontend/src/app/components/pool/pool-preview.component.ts new file mode 100644 index 000000000..2799dc34b --- /dev/null +++ b/frontend/src/app/components/pool/pool-preview.component.ts @@ -0,0 +1,187 @@ +import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { EChartsOption, graphic } from 'echarts'; +import { Observable, of } from 'rxjs'; +import { map, switchMap, catchError } from 'rxjs/operators'; +import { PoolStat } from 'src/app/interfaces/node-api.interface'; +import { ApiService } from 'src/app/services/api.service'; +import { StateService } from 'src/app/services/state.service'; +import { formatNumber } from '@angular/common'; +import { SeoService } from 'src/app/services/seo.service'; +import { OpenGraphService } from 'src/app/services/opengraph.service'; + +@Component({ + selector: 'app-pool-preview', + templateUrl: './pool-preview.component.html', + styleUrls: ['./pool-preview.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class PoolPreviewComponent implements OnInit { + formatNumber = formatNumber; + poolStats$: Observable; + isLoading = true; + imageLoaded = false; + lastImgSrc: string = ''; + + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + + slug: string = undefined; + + constructor( + @Inject(LOCALE_ID) public locale: string, + private apiService: ApiService, + private route: ActivatedRoute, + public stateService: StateService, + private seoService: SeoService, + private openGraphService: OpenGraphService, + ) { + } + + ngOnInit(): void { + this.poolStats$ = this.route.params.pipe(map((params) => params.slug)) + .pipe( + switchMap((slug: any) => { + this.isLoading = true; + this.imageLoaded = false; + this.slug = slug; + this.openGraphService.waitFor('pool-hash-' + this.slug); + this.openGraphService.waitFor('pool-stats-' + this.slug); + this.openGraphService.waitFor('pool-chart-' + this.slug); + this.openGraphService.waitFor('pool-img-' + this.slug); + return this.apiService.getPoolHashrate$(this.slug) + .pipe( + switchMap((data) => { + this.isLoading = false; + this.prepareChartOptions(data.map(val => [val.timestamp * 1000, val.avgHashrate])); + this.openGraphService.waitOver('pool-hash-' + this.slug); + return [slug]; + }), + catchError(() => { + this.isLoading = false; + this.openGraphService.fail('pool-hash-' + this.slug); + return of([slug]); + }) + ); + }), + switchMap((slug) => { + return this.apiService.getPoolStats$(slug).pipe( + catchError(() => { + this.isLoading = false; + this.openGraphService.fail('pool-stats-' + this.slug); + return of(null); + }) + ); + }), + map((poolStats) => { + if (poolStats == null) { + return null; + } + + this.seoService.setTitle(poolStats.pool.name); + let regexes = '"'; + for (const regex of poolStats.pool.regexes) { + regexes += regex + '", "'; + } + poolStats.pool.regexes = regexes.slice(0, -3); + poolStats.pool.addresses = poolStats.pool.addresses; + + if (poolStats.reportedHashrate) { + poolStats.luck = poolStats.estimatedHashrate / poolStats.reportedHashrate * 100; + } + + this.openGraphService.waitOver('pool-stats-' + this.slug); + + const logoSrc = `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'; + if (logoSrc === this.lastImgSrc) { + this.openGraphService.waitOver('pool-img-' + this.slug); + } + this.lastImgSrc = logoSrc; + return Object.assign({ + logo: logoSrc + }, poolStats); + }), + catchError(() => { + this.isLoading = false; + this.openGraphService.fail('pool-stats-' + this.slug); + return of(null); + }) + ); + } + + prepareChartOptions(data) { + let title: object; + if (data.length === 0) { + title = { + textStyle: { + color: 'grey', + fontSize: 15 + }, + text: $localize`:@@23555386d8af1ff73f297e89dd4af3f4689fb9dd:Indexing blocks`, + left: 'center', + top: 'center' + }; + } + + this.chartOptions = { + title: title, + animation: false, + color: [ + new graphic.LinearGradient(0, 0, 0, 0.65, [ + { offset: 0, color: '#F4511E' }, + { offset: 0.25, color: '#FB8C00' }, + { offset: 0.5, color: '#FFB300' }, + { offset: 0.75, color: '#FDD835' }, + { offset: 1, color: '#7CB342' } + ]), + '#D81B60', + ], + grid: { + left: 15, + right: 15, + bottom: 15, + top: 15, + show: false, + }, + xAxis: data.length === 0 ? undefined : { + type: 'time', + show: false, + }, + yAxis: data.length === 0 ? undefined : [ + { + type: 'value', + show: false, + }, + ], + series: data.length === 0 ? undefined : [ + { + zlevel: 0, + name: 'Hashrate', + showSymbol: false, + symbol: 'none', + data: data, + type: 'line', + lineStyle: { + width: 4, + }, + }, + ], + }; + } + + onChartReady(): void { + this.openGraphService.waitOver('pool-chart-' + this.slug); + } + + onImageLoad(): void { + this.imageLoaded = true; + this.openGraphService.waitOver('pool-img-' + this.slug); + } + + onImageFail(): void { + this.imageLoaded = false; + this.openGraphService.waitOver('pool-img-' + this.slug); + } +} diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 0ff82899f..f25e2a012 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -190,6 +190,24 @@ + + Diagram + + + + + + + 24"> + Show more + + Show less + + + + + + Inputs & Outputs @@ -283,6 +301,36 @@ + + Diagram + + + + + + + + + + + + + + + + + + + + + + + + + + + + Inputs & Outputs diff --git a/frontend/src/app/components/transaction/transaction.component.scss b/frontend/src/app/components/transaction/transaction.component.scss index 4628c35f9..ec514369f 100644 --- a/frontend/src/app/components/transaction/transaction.component.scss +++ b/frontend/src/app/components/transaction/transaction.component.scss @@ -73,6 +73,24 @@ } } +.graph-container { + position: relative; + width: 100%; + background: #181b2d; + padding: 10px; + padding-bottom: 0; +} + +.toggle-wrapper { + width: 100%; + text-align: center; + margin: 1.25em 0 0; +} + +.graph-toggle { + margin: auto; +} + @media (max-width: 767.98px) { .mobile-bottomcol { margin-top: 15px; diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 9a2629f08..be6460167 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Component, OnInit, AfterViewInit, OnDestroy, HostListener, ViewChild, ElementRef } from '@angular/core'; import { ElectrsApiService } from '../../services/electrs-api.service'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { @@ -24,7 +24,7 @@ import { LiquidUnblinding } from './liquid-ublinding'; templateUrl: './transaction.component.html', styleUrls: ['./transaction.component.scss'], }) -export class TransactionComponent implements OnInit, OnDestroy { +export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { network = ''; tx: Transaction; txId: string; @@ -47,6 +47,14 @@ export class TransactionComponent implements OnInit, OnDestroy { timeAvg$: Observable; liquidUnblinding = new LiquidUnblinding(); outputIndex: number; + graphExpanded: boolean = false; + graphWidth: number = 1000; + graphHeight: number = 360; + maxInOut: number = 0; + tooltipPosition: { x: number, y: number }; + + @ViewChild('graphContainer') + graphContainer: ElementRef; constructor( private route: ActivatedRoute, @@ -167,6 +175,7 @@ export class TransactionComponent implements OnInit, OnDestroy { this.waitingForTransaction = false; this.setMempoolBlocksSubscription(); this.websocketService.startTrackTransaction(tx.txid); + this.setupGraph(); if (!tx.status.confirmed && tx.firstSeen) { this.transactionTime = tx.firstSeen; @@ -222,6 +231,10 @@ export class TransactionComponent implements OnInit, OnDestroy { }); } + ngAfterViewInit(): void { + this.setGraphSize(); + } + handleLoadElectrsTransactionError(error: any): Observable { if (error.status === 404 && /^[a-fA-F0-9]{64}$/.test(this.txId)) { this.websocketService.startMultiTrackTransaction(this.txId); @@ -284,6 +297,26 @@ export class TransactionComponent implements OnInit, OnDestroy { return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1); } + setupGraph() { + this.maxInOut = Math.min(250, Math.max(this.tx?.vin?.length || 1, this.tx?.vout?.length + 1 || 1)); + this.graphHeight = Math.min(360, this.maxInOut * 80); + } + + expandGraph() { + this.graphExpanded = true; + } + + collapseGraph() { + this.graphExpanded = false; + } + + @HostListener('window:resize', ['$event']) + setGraphSize(): void { + if (this.graphContainer) { + this.graphWidth = this.graphContainer.nativeElement.clientWidth - 24; + } + } + ngOnDestroy() { this.subscription.unsubscribe(); this.fetchCpfpSubscription.unsubscribe(); diff --git a/frontend/src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html b/frontend/src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html new file mode 100644 index 000000000..563e6ed00 --- /dev/null +++ b/frontend/src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html @@ -0,0 +1,56 @@ + + + {{ line.rest }} + + other inputs + other outputs + + + + + + Coinbase + + + + + + Peg In + + + + + + Peg Out + + + {{ line.pegout.slice(0, -4) }} + {{ line.pegout.slice(-4) }} + + + + + + + + Input + Output + Fee + + #{{ line.index }} + + Confidential + + + {{ line.address.slice(0, -4) }} + {{ line.address.slice(-4) }} + + + diff --git a/frontend/src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.scss b/frontend/src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.scss new file mode 100644 index 000000000..d0551f2c8 --- /dev/null +++ b/frontend/src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.scss @@ -0,0 +1,38 @@ +.bowtie-graph-tooltip { + position: absolute; + background: rgba(#11131f, 0.95); + border-radius: 4px; + box-shadow: 1px 1px 10px rgba(0,0,0,0.5); + color: #b1b1b1; + padding: 10px 15px; + text-align: left; + pointer-events: none; + max-width: 300px; + + p { + margin: 0; + white-space: nowrap; + } + + .address { + width: 100%; + max-width: 100%; + display: flex; + flex-direction: row; + align-items: baseline; + justify-content: flex-start; + + .first { + flex-grow: 0; + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; + margin-right: -2px; + } + + .last-four { + flex-shrink: 0; + flex-grow: 0; + } + } +} \ No newline at end of file diff --git a/frontend/src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.ts b/frontend/src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.ts new file mode 100644 index 000000000..413bb68c0 --- /dev/null +++ b/frontend/src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.ts @@ -0,0 +1,48 @@ +import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStrategy } from '@angular/core'; +import { TransactionStripped } from 'src/app/interfaces/websocket.interface'; + +interface Xput { + type: 'input' | 'output' | 'fee'; + value?: number; + index?: number; + address?: string; + rest?: number; + coinbase?: boolean; + pegin?: boolean; + pegout?: string; + confidential?: boolean; +} + +@Component({ + selector: 'app-tx-bowtie-graph-tooltip', + templateUrl: './tx-bowtie-graph-tooltip.component.html', + styleUrls: ['./tx-bowtie-graph-tooltip.component.scss'], +}) +export class TxBowtieGraphTooltipComponent implements OnChanges { + @Input() line: Xput | void; + @Input() cursorPosition: { x: number, y: number }; + + tooltipPosition = { x: 0, y: 0 }; + + @ViewChild('tooltip') tooltipElement: ElementRef; + + constructor() {} + + ngOnChanges(changes): void { + if (changes.cursorPosition && changes.cursorPosition.currentValue) { + let x = Math.max(10, changes.cursorPosition.currentValue.x - 50); + let y = changes.cursorPosition.currentValue.y + 20; + if (this.tooltipElement) { + const elementBounds = this.tooltipElement.nativeElement.getBoundingClientRect(); + const parentBounds = this.tooltipElement.nativeElement.offsetParent.getBoundingClientRect(); + if ((parentBounds.left + x + elementBounds.width) > parentBounds.right) { + x = Math.max(0, parentBounds.width - elementBounds.width - 10); + } + if (y + elementBounds.height > parentBounds.height) { + y = y - elementBounds.height - 20; + } + } + this.tooltipPosition = { x, y }; + } + } +} diff --git a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.html b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.html index c4771c58c..03056cd53 100644 --- a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.html +++ b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.html @@ -1,44 +1,82 @@ - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.scss b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.scss index 6de41b95f..9cacb7d4b 100644 --- a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.scss +++ b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.scss @@ -11,5 +11,19 @@ &.fee { stroke: url(#fee-gradient); } + + &:hover { + z-index: 10; + cursor: pointer; + &.input { + stroke: url(#input-hover-gradient); + } + &.output { + stroke: url(#output-hover-gradient); + } + &.fee { + stroke: url(#fee-hover-gradient); + } + } } } diff --git a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts index e45acc26c..78a865b89 100644 --- a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts +++ b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, Input, OnChanges } from '@angular/core'; +import { Component, OnInit, Input, OnChanges, HostListener } from '@angular/core'; import { Transaction } from '../../interfaces/electrs.interface'; interface SvgLine { @@ -7,6 +7,20 @@ interface SvgLine { class?: string; } +interface Xput { + type: 'input' | 'output' | 'fee'; + value?: number; + index?: number; + address?: string; + rest?: number; + coinbase?: boolean; + pegin?: boolean; + pegout?: string; + confidential?: boolean; +} + +const lineLimit = 250; + @Component({ selector: 'tx-bowtie-graph', templateUrl: './tx-bowtie-graph.component.html', @@ -20,11 +34,17 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { @Input() combinedWeight = 100; @Input() minWeight = 2; // @Input() maxStrands = 24; // number of inputs/outputs to keep fully on-screen. + @Input() tooltip = false; + inputData: Xput[]; + outputData: Xput[]; inputs: SvgLine[]; outputs: SvgLine[]; middle: SvgLine; + midWidth: number; isLiquid: boolean = false; + hoverLine: Xput | void = null; + tooltipPosition = { x: 0, y: 0 }; gradientColors = { '': ['#9339f4', '#105fb0'], @@ -44,28 +64,68 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { ngOnInit(): void { this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet'); this.gradient = this.gradientColors[this.network]; + this.midWidth = Math.min(50, Math.ceil(this.width / 20)); this.initGraph(); } ngOnChanges(): void { this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet'); this.gradient = this.gradientColors[this.network]; + this.midWidth = Math.min(50, Math.ceil(this.width / 20)); this.initGraph(); } initGraph(): void { const totalValue = this.calcTotalValue(this.tx); - const voutWithFee = this.tx.vout.map(v => { return { type: v.scriptpubkey_type === 'fee' ? 'fee' : 'output', value: v?.value }; }); + let voutWithFee = this.tx.vout.map(v => { + return { + type: v.scriptpubkey_type === 'fee' ? 'fee' : 'output', + value: v?.value, + address: v?.scriptpubkey_address || v?.scriptpubkey_type?.toUpperCase(), + pegout: v?.pegout?.scriptpubkey_address, + confidential: (this.isLiquid && v?.value === undefined), + } as Xput; + }); if (this.tx.fee && !this.isLiquid) { voutWithFee.unshift({ type: 'fee', value: this.tx.fee }); } + const outputCount = voutWithFee.length; - this.inputs = this.initLines('in', this.tx.vin.map(v => { return {type: 'input', value: v?.prevout?.value }; }), totalValue, this.maxStrands); + let truncatedInputs = this.tx.vin.map(v => { + return { + type: 'input', + value: v?.prevout?.value, + address: v?.prevout?.scriptpubkey_address || v?.prevout?.scriptpubkey_type?.toUpperCase(), + coinbase: v?.is_coinbase, + pegin: v?.is_pegin, + confidential: (this.isLiquid && v?.prevout?.value === undefined), + } as Xput; + }); + + if (truncatedInputs.length > lineLimit) { + const valueOfRest = truncatedInputs.slice(lineLimit).reduce((r, v) => { + return r + (v.value || 0); + }, 0); + truncatedInputs = truncatedInputs.slice(0, lineLimit); + truncatedInputs.push({ type: 'input', value: valueOfRest, rest: this.tx.vin.length - lineLimit }); + } + if (voutWithFee.length > lineLimit) { + const valueOfRest = voutWithFee.slice(lineLimit).reduce((r, v) => { + return r + (v.value || 0); + }, 0); + voutWithFee = voutWithFee.slice(0, lineLimit); + voutWithFee.push({ type: 'output', value: valueOfRest, rest: outputCount - lineLimit }); + } + + this.inputData = truncatedInputs; + this.outputData = voutWithFee; + + this.inputs = this.initLines('in', truncatedInputs, totalValue, this.maxStrands); this.outputs = this.initLines('out', voutWithFee, totalValue, this.maxStrands); this.middle = { - path: `M ${(this.width / 2) - 50} ${(this.height / 2) + 0.5} L ${(this.width / 2) + 50} ${(this.height / 2) + 0.5}`, + path: `M ${(this.width / 2) - this.midWidth} ${(this.height / 2) + 0.5} L ${(this.width / 2) + this.midWidth} ${(this.height / 2) + 0.5}`, style: `stroke-width: ${this.combinedWeight + 0.5}; stroke: ${this.gradient[1]}` }; } @@ -95,7 +155,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { } } - initLines(side: 'in' | 'out', xputs: { type: string, value: number | void }[], total: number, maxVisibleStrands: number): SvgLine[] { + initLines(side: 'in' | 'out', xputs: Xput[], total: number, maxVisibleStrands: number): SvgLine[] { if (!total) { const weights = xputs.map((put): number => this.combinedWeight / xputs.length); return this.linesFromWeights(side, xputs, weights, maxVisibleStrands); @@ -116,7 +176,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { } } - linesFromWeights(side: 'in' | 'out', xputs: { type: string, value: number | void }[], weights: number[], maxVisibleStrands: number) { + linesFromWeights(side: 'in' | 'out', xputs: Xput[], weights: number[], maxVisibleStrands: number) { const lines = []; // actual displayed line thicknesses const minWeights = weights.map((w) => Math.max(this.minWeight - 1, w) + 1); @@ -158,7 +218,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { makePath(side: 'in' | 'out', outer: number, inner: number, weight: number): string { const start = side === 'in' ? (weight * 0.5) : this.width - (weight * 0.5); - const center = this.width / 2 + (side === 'in' ? -45 : 45 ); + const center = this.width / 2 + (side === 'in' ? -(this.midWidth * 0.9) : (this.midWidth * 0.9) ); const midpoint = (start + center) / 2; // correct for svg horizontal gradient bug if (Math.round(outer) === Math.round(inner)) { @@ -169,9 +229,32 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { makeStyle(minWeight, type): string { if (type === 'fee') { - return `stroke-width: ${minWeight}; stroke: url(#fee-gradient)`; + return `stroke-width: ${minWeight}`; } else { return `stroke-width: ${minWeight}`; } } + + @HostListener('pointermove', ['$event']) + onPointerMove(event) { + this.tooltipPosition = { x: event.offsetX, y: event.offsetY }; + } + + onHover(event, side, index): void { + if (side === 'input') { + this.hoverLine = { + ...this.inputData[index], + index + }; + } else { + this.hoverLine = { + ...this.outputData[index], + index + }; + } + } + + onBlur(event, side, index): void { + this.hoverLine = null; + } } diff --git a/frontend/src/app/previews.module.ts b/frontend/src/app/previews.module.ts index 166670ced..2e8dbdc75 100644 --- a/frontend/src/app/previews.module.ts +++ b/frontend/src/app/previews.module.ts @@ -7,20 +7,22 @@ import { PreviewsRoutingModule } from './previews.routing.module'; import { TransactionPreviewComponent } from './components/transaction/transaction-preview.component'; import { BlockPreviewComponent } from './components/block/block-preview.component'; import { AddressPreviewComponent } from './components/address/address-preview.component'; +import { PoolPreviewComponent } from './components/pool/pool-preview.component'; import { MasterPagePreviewComponent } from './components/master-page-preview/master-page-preview.component'; @NgModule({ declarations: [ TransactionPreviewComponent, BlockPreviewComponent, AddressPreviewComponent, + PoolPreviewComponent, MasterPagePreviewComponent, ], imports: [ CommonModule, SharedModule, RouterModule, - GraphsModule, PreviewsRoutingModule, + GraphsModule, ], }) export class PreviewsModule { } diff --git a/frontend/src/app/previews.routing.module.ts b/frontend/src/app/previews.routing.module.ts index 5ac13c36d..c2ad8db5f 100644 --- a/frontend/src/app/previews.routing.module.ts +++ b/frontend/src/app/previews.routing.module.ts @@ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router'; import { TransactionPreviewComponent } from './components/transaction/transaction-preview.component'; import { BlockPreviewComponent } from './components/block/block-preview.component'; import { AddressPreviewComponent } from './components/address/address-preview.component'; +import { PoolPreviewComponent } from './components/pool/pool-preview.component'; import { MasterPagePreviewComponent } from './components/master-page-preview/master-page-preview.component'; const routes: Routes = [ @@ -24,6 +25,10 @@ const routes: Routes = [ children: [], component: TransactionPreviewComponent }, + { + path: 'mining/pool/:slug', + component: PoolPreviewComponent + }, { path: 'lightning', loadChildren: () => import('./lightning/lightning-previews.module').then(m => m.LightningPreviewsModule) diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index be4ba2fe0..c4973d75c 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -61,6 +61,7 @@ import { FeesBoxComponent } from '../components/fees-box/fees-box.component'; import { DifficultyComponent } from '../components/difficulty/difficulty.component'; import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component'; import { TxBowtieGraphComponent } from '../components/tx-bowtie-graph/tx-bowtie-graph.component'; +import { TxBowtieGraphTooltipComponent } from '../components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component'; import { PrivacyPolicyComponent } from '../components/privacy-policy/privacy-policy.component'; import { TrademarkPolicyComponent } from '../components/trademark-policy/trademark-policy.component'; import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component'; @@ -134,6 +135,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati FeesBoxComponent, DifficultyComponent, TxBowtieGraphComponent, + TxBowtieGraphTooltipComponent, TermsOfServiceComponent, PrivacyPolicyComponent, TrademarkPolicyComponent, @@ -236,6 +238,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati FeesBoxComponent, DifficultyComponent, TxBowtieGraphComponent, + TxBowtieGraphTooltipComponent, TermsOfServiceComponent, PrivacyPolicyComponent, TrademarkPolicyComponent, diff --git a/unfurler/src/routes.ts b/unfurler/src/routes.ts index 4c25bf93b..3dfa66b5f 100644 --- a/unfurler/src/routes.ts +++ b/unfurler/src/routes.ts @@ -61,7 +61,16 @@ const routes = { }, mining: { title: "Mining", - fallbackImg: '/resources/previews/mining.png' + fallbackImg: '/resources/previews/mining.png', + routes: { + pool: { + render: true, + params: 1, + getTitle(path) { + return `Mining Pool: ${path[0]}`; + } + } + } } };
Coinbase
Peg In
Peg Out
+ {{ line.pegout.slice(0, -4) }} + {{ line.pegout.slice(-4) }} +
+ + Input + Output + Fee + + #{{ line.index }} +
Confidential
+ {{ line.address.slice(0, -4) }} + {{ line.address.slice(-4) }} +