Merge pull request #4785 from mempool/natsoni/bowtie-tooltip-price

Display more accurate price data on tx bowtie tooltip
This commit is contained in:
softsimon 2024-03-18 14:46:39 +09:00 committed by GitHub
commit b7d9efebd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 155 additions and 12 deletions

View File

@ -8,9 +8,12 @@
}}
</span>
<ng-template #noblockconversion>
<span class="fiat">{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ (conversions[currency] > -1 ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }}
<span class="fiat" *ngIf="!forceBlockConversion; else zeroValue">{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ (conversions[currency] > -1 ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }}
</span>
</ng-template>
<ng-template #zeroValue>
<span class="fiat">{{ 0 | fiatCurrency : digitsInfo : currency }}</span>
</ng-template>
</ng-container>
<ng-template #viewFiatVin>

View File

@ -24,6 +24,7 @@ export class AmountComponent implements OnInit, OnDestroy {
@Input() addPlus = false;
@Input() blockConversion: Price;
@Input() forceBtc: boolean = false;
@Input() forceBlockConversion: boolean = false; // true = displays fiat price as 0 if blockConversion is undefined instead of falling back to conversions
constructor(
private stateService: StateService,

View File

@ -44,6 +44,28 @@
<span *ngSwitchCase="'fee'" i18n="transaction.fee|Transaction fee">Fee</span>
</ng-container>
<span *ngIf="line.type !== 'fee'"> #{{ line.index + 1 }}</span>
<ng-container [ngSwitch]="line.type">
<span *ngSwitchCase="'input'">
<ng-container *ngIf="line.status?.block_height">
<ng-container *ngIf="line.blockHeight; else noBlockHeight">
<ng-container *ngTemplateOutlet="nBlocksEarlier; context:{n: line.blockHeight - line?.status?.block_height, connector: false}"></ng-container>
</ng-container>
<ng-template #noBlockHeight>
<ng-container *ngTemplateOutlet="nBlocksEarlier; context:{n: chainTip + 1 - line?.status?.block_height, connector: false}"></ng-container>
</ng-template>
</ng-container>
</span>
<span *ngSwitchCase="'output'">
<ng-container *ngIf="line.blockHeight && line?.spent">
<ng-container *ngIf="line?.status?.block_height; else noBlockHeight">
<ng-container *ngTemplateOutlet="nBlocksLater; context:{n: line?.status?.block_height - line.blockHeight, connector: false}"></ng-container>
</ng-container>
<ng-template #noBlockHeight>
<ng-container *ngTemplateOutlet="nBlocksLater; context:{n: chainTip + 1 - line.blockHeight, connector: false}"></ng-container>
</ng-template>
</ng-container>
</span>
</ng-container>
</p>
<ng-container *ngIf="isConnector && line.txid">
<p>
@ -51,8 +73,26 @@
<app-truncate [text]="line.txid"></app-truncate>
</p>
<ng-container [ngSwitch]="line.type">
<p *ngSwitchCase="'input'"><span i18n="transaction.output">Output</span>&nbsp; #{{ line.vout + 1 }}</p>
<p *ngSwitchCase="'output'"><span i18n="transaction.input">Input</span>&nbsp; #{{ line.vin + 1 }}</p>
<p *ngSwitchCase="'input'"><span i18n="transaction.output">Output</span>&nbsp; #{{ line.vout + 1 }}
<ng-container *ngIf="line.status?.block_height">
<ng-container *ngIf="line.blockHeight; else noBlockHeight">
<ng-container *ngTemplateOutlet="nBlocksEarlier; context:{n: line.blockHeight - line?.status?.block_height, connector: true}"></ng-container>
</ng-container>
<ng-template #noBlockHeight>
<ng-container *ngTemplateOutlet="nBlocksEarlier; context:{n: chainTip + 1 - line?.status?.block_height, connector: true}"></ng-container>
</ng-template>
</ng-container>
</p>
<p *ngSwitchCase="'output'"><span i18n="transaction.input">Input</span>&nbsp; #{{ line.vin + 1 }}
<ng-container *ngIf="line.blockHeight">
<ng-container *ngIf="line?.status?.block_height; else noBlockHeight">
<ng-container *ngTemplateOutlet="nBlocksLater; context:{n: line?.status?.block_height - line.blockHeight, connector: true}"></ng-container>
</ng-container>
<ng-template #noBlockHeight>
<ng-container *ngTemplateOutlet="nBlocksLater; context:{n: chainTip + 1 - line.blockHeight, connector: true}"></ng-container>
</ng-template>
</ng-container>
</p>
</ng-container>
</ng-container>
<p *ngIf="line.displayValue == null && line.confidential" i18n="shared.confidential">Confidential</p>
@ -66,7 +106,7 @@
</ng-template>
</ng-template>
<ng-template #defaultOutput>
<app-amount [blockConversion]="blockConversion" [satoshis]="line.displayValue"></app-amount>
<app-amount [blockConversion]="isConnector ? blockConversions[line?.status?.block_time] : blockConversions[line?.timestamp]" [satoshis]="line.displayValue" [forceBlockConversion]="isConnector && line?.status?.block_time"></app-amount>
</ng-template>
</p>
<p *ngIf="line.type !== 'fee' && line.address" class="address">
@ -77,4 +117,42 @@
<ng-template #assetBox let-item>
{{ item.displayValue / pow(10, assetsMinimal[item.asset][3]) | number: '1.' + assetsMinimal[item.asset][3] + '-' + assetsMinimal[item.asset][3] }} <span class="symbol">{{ assetsMinimal[item.asset][1] }}</span>
</ng-template>
<ng-template #oneBlockEarlier>
<span i18n="shared.one-block-earlier">1 block earlier</span>
</ng-template>
<ng-template #oneBlockLater>
<span i18n="shared.one-block-later">1 block later</span>
</ng-template>
<ng-template #inTheSameBlock>
<span i18n="shared.in-the-same-block">in the same block</span>
</ng-template>
<ng-template #nBlocksEarlier let-n="n" let-connector="connector">
(<span *ngIf="!connector">prevout </span>
<ng-container *ngIf="n > 1">
<span>{{ n }} <ng-container i18n="shared.n-blocks-earlier">blocks earlier</ng-container>)</span>
</ng-container>
<ng-container *ngIf="n === 1">
<span><ng-container *ngTemplateOutlet="oneBlockEarlier"></ng-container>)</span>
</ng-container>
<ng-container *ngIf="n === 0">
<span><ng-container *ngTemplateOutlet="inTheSameBlock"></ng-container>)</span>
</ng-container>
</ng-template>
<ng-template #nBlocksLater let-n="n" let-connector="connector">
(<span *ngIf="!connector" i18n="shared.spent">spent </span>
<ng-container *ngIf="n > 1">
<span>{{ n }} <ng-container i18n="shared.n-blocks-later">blocks later</ng-container>)</span>
</ng-container>
<ng-container *ngIf="n === 1">
<span><ng-container *ngTemplateOutlet="oneBlockLater"></ng-container>)</span>
</ng-container>
<ng-container *ngIf="n === 0">
<span><ng-container *ngTemplateOutlet="inTheSameBlock"></ng-container>)</span>
</ng-container>
</ng-template>

View File

@ -7,7 +7,7 @@
padding: 10px 15px;
text-align: left;
pointer-events: none;
max-width: 300px;
max-width: 350px;
p {
margin: 0;

View File

@ -2,6 +2,7 @@ import { Component, ElementRef, ViewChild, Input, OnChanges, OnInit } from '@ang
import { Subscription, of, switchMap, tap } from 'rxjs';
import { Price, PriceService } from '../../services/price.service';
import { StateService } from '../../services/state.service';
import { ApiService } from '../../services/api.service';
import { environment } from '../../../environments/environment';
interface Xput {
@ -19,6 +20,9 @@ interface Xput {
pegout?: string;
confidential?: boolean;
timestamp?: number;
blockHeight?: number;
status?: any;
spent?: boolean;
asset?: string;
}
@ -34,8 +38,14 @@ export class TxBowtieGraphTooltipComponent implements OnChanges {
@Input() assetsMinimal: any;
tooltipPosition = { x: 0, y: 0 };
blockConversion: Price;
blockConversions: { [timestamp: number]: Price } = {};
inputStatus: { [index: number]: any } = {};
currency: string;
viewFiat: boolean;
chainTip: number;
currencyChangeSubscription: Subscription;
viewFiatSubscription: Subscription;
chainTipSubscription: Subscription;
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
@ -44,18 +54,37 @@ export class TxBowtieGraphTooltipComponent implements OnChanges {
constructor(
private priceService: PriceService,
private stateService: StateService,
private apiService: ApiService,
) {}
ngOnInit(): void {
this.currencyChangeSubscription = this.stateService.fiatCurrency$.subscribe(currency => {
this.currency = currency;
this.blockConversions = {};
this.inputStatus = {};
});
this.viewFiatSubscription = this.stateService.viewFiat$.subscribe(viewFiat => this.viewFiat = viewFiat);
this.chainTipSubscription = this.stateService.chainTip$.subscribe(tip => this.chainTip = tip);
}
ngOnChanges(changes): void {
if (changes.line?.currentValue) {
this.currencyChangeSubscription?.unsubscribe();
this.currencyChangeSubscription = this.stateService.fiatCurrency$.pipe(
switchMap((currency) => {
return changes.line?.currentValue.timestamp ? this.priceService.getBlockPrice$(changes.line?.currentValue.timestamp, true, currency).pipe(
tap((price) => this.blockConversion = price),
) : of(undefined);
if (changes.line.currentValue.type === 'input') {
if (!this.inputStatus[changes.line.currentValue.index]) {
this.apiService.getTransactionStatus$(changes.line.currentValue.txid).pipe(
tap((status) => {
changes.line.currentValue.status = status;
this.inputStatus[changes.line.currentValue.index] = status;
this.fetchPrices(changes);
})
).subscribe();
} else {
changes.line.currentValue.status = this.inputStatus[changes.line.currentValue.index];
this.fetchPrices(changes);
}
} else {
this.fetchPrices(changes);
}
}
if (changes.cursorPosition && changes.cursorPosition.currentValue) {
@ -75,7 +104,32 @@ export class TxBowtieGraphTooltipComponent implements OnChanges {
}
}
fetchPrices(changes: any) {
if (!this.currency || !this.viewFiat) return;
if (this.isConnector) { // If the tooltip is on a connector, we fetch prices at the time of the input / output
if (['input', 'output'].includes(changes.line.currentValue.type) && changes.line.currentValue?.status?.block_time && !this.blockConversions?.[changes.line.currentValue?.status.block_time]) {
this.priceService.getBlockPrice$(changes.line.currentValue?.status.block_time, true, this.currency).pipe(
tap((price) => this.blockConversions[changes.line.currentValue.status.block_time] = price),
).subscribe();
}
} else { // If the tooltip is on the transaction itself, we fetch prices at the time of the transaction
if (changes.line.currentValue.timestamp && !this.blockConversions[changes.line.currentValue.timestamp]) {
if (changes.line.currentValue.timestamp) {
this.priceService.getBlockPrice$(changes.line.currentValue.timestamp, true, this.currency).pipe(
tap((price) => this.blockConversions[changes.line.currentValue.timestamp] = price),
).subscribe();
}
}
}
}
pow(base: number, exponent: number): number {
return Math.pow(base, exponent);
}
ngOnDestroy(): void {
this.currencyChangeSubscription?.unsubscribe();
this.viewFiatSubscription?.unsubscribe();
this.chainTipSubscription?.unsubscribe();
}
}

View File

@ -34,6 +34,7 @@ interface Xput {
pegout?: string;
confidential?: boolean;
timestamp?: number;
blockHeight?: number;
asset?: string;
}
@ -178,6 +179,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
pegout: v?.pegout?.scriptpubkey_address,
confidential: (this.isLiquid && v?.value === undefined),
timestamp: this.tx.status.block_time,
blockHeight: this.tx.status.block_height,
asset: v?.asset,
} as Xput;
});
@ -200,6 +202,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
pegin: v?.is_pegin,
confidential: (this.isLiquid && v?.prevout?.value === undefined),
timestamp: this.tx.status.block_time,
blockHeight: this.tx.status.block_height,
asset: v?.prevout?.asset,
} as Xput;
});

View File

@ -240,6 +240,10 @@ export class ApiService {
return this.httpClient.post<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'});
}
getTransactionStatus$(txid: string): Observable<any> {
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + txid + '/status');
}
listPools$(interval: string | undefined) : Observable<any> {
return this.httpClient.get<any>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pools` +