custom dashboard wallet widgets

This commit is contained in:
Mononaut 2024-07-25 22:34:52 +00:00
parent 4d06636d83
commit 64baade3b3
No known key found for this signature in database
GPG Key ID: A3F058E41374C04E
18 changed files with 150 additions and 23 deletions

View File

@ -83,7 +83,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
this.isLoading = true; this.isLoading = true;
if (!this.address || !this.stats) { if (!this.addressSummary$ && (!this.address || !this.stats)) {
return; return;
} }
if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) { if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
@ -144,15 +144,16 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
} }
prepareChartOptions(summary: AddressTxSummary[]) { prepareChartOptions(summary: AddressTxSummary[]) {
if (!summary || !this.stats) { if (!summary) {
return; return;
} }
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum); const total = this.stats ? (this.stats.funded_txo_sum - this.stats.spent_txo_sum) : summary.reduce((acc, tx) => acc + tx.value, 0);
let runningTotal = total;
const processData = summary.map(d => { const processData = summary.map(d => {
const balance = total; const balance = runningTotal;
const fiatBalance = total * d.price / 100_000_000; const fiatBalance = runningTotal * d.price / 100_000_000;
total -= d.value; runningTotal -= d.value;
return { return {
time: d.time * 1000, time: d.time * 1000,
balance, balance,
@ -172,7 +173,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
this.fiatData = this.fiatData.filter(d => d[0] >= startFiat); this.fiatData = this.fiatData.filter(d => d[0] >= startFiat);
} }
this.data.push( this.data.push(
{value: [now, this.stats.funded_txo_sum - this.stats.spent_txo_sum], symbol: 'none', tooltip: { show: false }} {value: [now, total], symbol: 'none', tooltip: { show: false }}
); );
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0); const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0);

View File

@ -12,7 +12,7 @@
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate> <app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
</a> </a>
</td> </td>
<td class="table-cell-satoshis"><app-amount [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount></td> <td class="table-cell-satoshis"><app-amount [satoshis]="transaction.value" [digitsInfo]="getAmountDigits(transaction.value)" [noFiat]="true"></app-amount></td>
<td class="table-cell-fiat" ><app-fiat [value]="transaction.value" [blockConversion]="transaction.price" digitsInfo="1.0-0"></app-fiat></td> <td class="table-cell-fiat" ><app-fiat [value]="transaction.value" [blockConversion]="transaction.price" digitsInfo="1.0-0"></app-fiat></td>
<td class="table-cell-date"><app-time kind="since" [time]="transaction.time" [fastRender]="true" [showTooltip]="true"></app-time></td> <td class="table-cell-date"><app-time kind="since" [time]="transaction.time" [fastRender]="true" [showTooltip]="true"></app-time></td>
</tr> </tr>

View File

@ -43,7 +43,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
startAddressSubscription(): void { startAddressSubscription(): void {
this.isLoading = true; this.isLoading = true;
if (!this.address || !this.addressInfo) { if (!this.addressSummary$ && (!this.address || !this.addressInfo)) {
return; return;
} }
this.transactions$ = (this.addressSummary$ || (this.isPubkey this.transactions$ = (this.addressSummary$ || (this.isPubkey
@ -55,7 +55,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
}) })
)).pipe( )).pipe(
map(summary => { map(summary => {
return summary?.slice(0, 6); return summary?.filter(tx => Math.abs(tx.value) >= 1000000)?.slice(0, 6);
}), }),
switchMap(txs => { switchMap(txs => {
return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, txs.length < 3, this.currency).pipe( return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, txs.length < 3, this.currency).pipe(
@ -68,6 +68,12 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
)))); ))));
}) })
); );
}
getAmountDigits(value: number): string {
const decimals = Math.max(0, 4 - Math.ceil(Math.log10(Math.abs(value / 100_000_000))));
return `1.${decimals}-${decimals}`;
} }
ngOnDestroy(): void { ngOnDestroy(): void {

View File

@ -4,10 +4,10 @@
<div class="item"> <div class="item">
<h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5> <h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5>
<div class="card-text"> <div class="card-text">
{{ ((addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span> {{ ((total) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
</div> </div>
<div class="symbol"> <div class="symbol">
<app-fiat [value]="(addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum)"></app-fiat> <app-fiat [value]="(total)"></app-fiat>
</div> </div>
</div> </div>
<div class="item"> <div class="item">

View File

@ -19,6 +19,7 @@ export class BalanceWidgetComponent implements OnInit, OnChanges {
isLoading: boolean = true; isLoading: boolean = true;
error: any; error: any;
total: number = 0;
delta7d: number = 0; delta7d: number = 0;
delta30d: number = 0; delta30d: number = 0;
@ -34,7 +35,7 @@ export class BalanceWidgetComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
this.isLoading = true; this.isLoading = true;
if (!this.address || !this.addressInfo) { if (!this.addressSummary$ && (!this.address || !this.addressInfo)) {
return; return;
} }
(this.addressSummary$ || (this.isPubkey (this.addressSummary$ || (this.isPubkey
@ -57,6 +58,7 @@ export class BalanceWidgetComponent implements OnInit, OnChanges {
calculateStats(summary: AddressTxSummary[]): void { calculateStats(summary: AddressTxSummary[]): void {
let weekTotal = 0; let weekTotal = 0;
let monthTotal = 0; let monthTotal = 0;
this.total = this.addressInfo ? this.addressInfo.chain_stats.funded_txo_sum - this.addressInfo.chain_stats.spent_txo_sum : summary.reduce((acc, tx) => acc + tx.value, 0);
const weekAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (7 * 24 * 60 * 60 * 1000)).getTime()) / 1000; const weekAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (7 * 24 * 60 * 60 * 1000)).getTime()) / 1000;
const monthAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (30 * 24 * 60 * 60 * 1000)).getTime()) / 1000; const monthAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (30 * 24 * 60 * 60 * 1000)).getTime()) / 1000;

View File

@ -257,6 +257,36 @@
</div> </div>
</div> </div>
} }
@case ('walletBalance') {
<div class="col card-wrapper" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="main-title" i18n="dashboard.treasury">Treasury</div>
<app-balance-widget [addressSummary$]="walletSummary$"></app-balance-widget>
</div>
}
@case ('wallet') {
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card graph-card">
<div class="card-body">
<span class="title-link">
<h5 class="card-title d-inline" i18n="dashboard.balance-history">Balance History</h5>
</span>
<app-address-graph [addressSummary$]="walletSummary$" [period]="widget.props.period || 'all'" [widget]="true" [height]="graphHeight"></app-address-graph>
</div>
</div>
</div>
}
@case ('walletTransactions') {
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card">
<div class="card-body">
<span class="title-link">
<h5 class="card-title d-inline" i18n="dashboard.treasury-transactions">Treasury Transactions</h5>
</span>
<app-address-transactions-widget [addressSummary$]="walletSummary$"></app-address-transactions-widget>
</div>
</div>
</div>
}
@case ('twitter') { @case ('twitter') {
<div class="col" style="min-height:410px" [style.order]="isMobile && widget.mobileOrder || 8"> <div class="col" style="min-height:410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card graph-card"> <div class="card graph-card">

View File

@ -62,8 +62,10 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
widgets; widgets;
addressSubscription: Subscription; addressSubscription: Subscription;
walletSubscription: Subscription;
blockTxSubscription: Subscription; blockTxSubscription: Subscription;
addressSummary$: Observable<AddressTxSummary[]>; addressSummary$: Observable<AddressTxSummary[]>;
walletSummary$: Observable<AddressTxSummary[]>;
address: Address; address: Address;
goggleResolution = 82; goggleResolution = 82;
@ -107,6 +109,10 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
this.websocketService.stopTrackingAddress(); this.websocketService.stopTrackingAddress();
this.address = null; this.address = null;
} }
if (this.walletSubscription) {
this.walletSubscription.unsubscribe();
this.websocketService.stopTrackingWallet();
}
this.destroy$.next(1); this.destroy$.next(1);
this.destroy$.complete(); this.destroy$.complete();
} }
@ -260,6 +266,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
}); });
this.startAddressSubscription(); this.startAddressSubscription();
this.startWalletSubscription();
} }
handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) { handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {
@ -358,6 +365,51 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
} }
} }
startWalletSubscription(): void {
if (this.stateService.env.customize && this.stateService.env.customize.dashboard.widgets.some(w => w.props?.wallet)) {
const walletName = this.stateService.env.customize.dashboard.widgets.find(w => w.props?.wallet).props.wallet;
this.websocketService.startTrackingWallet(walletName);
this.walletSummary$ = this.apiService.getWallet$(walletName).pipe(
catchError(e => {
return of(null);
}),
map((walletTransactions) => {
const transactions = Object.values(walletTransactions).flatMap(wallet => wallet.transactions);
return this.deduplicateWalletTransactions(transactions);
}),
switchMap(initial => this.stateService.walletTransactions$.pipe(
startWith(null),
scan((summary, walletTransactions) => {
if (walletTransactions) {
const transactions: AddressTxSummary[] = [...summary, ...Object.values(walletTransactions).flat()];
return this.deduplicateWalletTransactions(transactions);
}
return summary;
}, initial)
)),
share(),
);
}
}
deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] {
const transactions = new Map<string, AddressTxSummary>();
for (const tx of walletTransactions) {
if (transactions.has(tx.txid)) {
transactions.get(tx.txid).value += tx.value;
} else {
transactions.set(tx.txid, tx);
}
}
return Array.from(transactions.values()).sort((a, b) => {
if (a.height === b.height) {
return b.tx_position - a.tx_position;
}
return b.height - a.height;
});
}
@HostListener('window:resize', ['$event']) @HostListener('window:resize', ['$event'])
onResize(): void { onResize(): void {
if (window.innerWidth >= 992) { if (window.innerWidth >= 992) {

View File

@ -6,7 +6,7 @@
<img [src]="enterpriseInfo.img" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}"> <img [src]="enterpriseInfo.img" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
} }
@if (enterpriseInfo?.header_img) { @if (enterpriseInfo?.header_img) {
<img *ngIf="enterpriseInfo.header_img" [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="60px" class="mr-3"> <img *ngIf="enterpriseInfo.header_img" [src]="enterpriseInfo?.header_img" [alt]="enterpriseInfo.title" height="60px" class="mr-3">
} @else { } @else {
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" width="500" height="126" class="mempool-logo" style="width: 200px; height: 50px"></app-svg-images> <app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" width="500" height="126" class="mempool-logo" style="width: 200px; height: 50px"></app-svg-images>
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 200px; height: 50px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images> <app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 200px; height: 50px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>

View File

@ -19,7 +19,7 @@
<a class="navbar-brand d-none d-md-flex" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)"> <a class="navbar-brand d-none d-md-flex" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState"> <ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
@if (enterpriseInfo?.header_img) { @if (enterpriseInfo?.header_img) {
<img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="48px" class="mr-3"> <img [src]="enterpriseInfo?.header_img" [alt]="enterpriseInfo.title" height="48px" class="mr-3">
} @else { } @else {
<ng-template [ngIf]="subdomain && enterpriseInfo"> <ng-template [ngIf]="subdomain && enterpriseInfo">
<div class="subdomain_container"> <div class="subdomain_container">
@ -39,7 +39,7 @@
<!-- Mobile --> <!-- Mobile -->
<a class="navbar-brand d-flex d-md-none justify-content-center" [ngClass]="{'dual-logos': subdomain, 'mr-0': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)"> <a class="navbar-brand d-flex d-md-none justify-content-center" [ngClass]="{'dual-logos': subdomain, 'mr-0': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
@if (enterpriseInfo?.header_img) { @if (enterpriseInfo?.header_img) {
<img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="42px"> <img [src]="enterpriseInfo?.header_img" [alt]="enterpriseInfo.title" height="42px">
} @else { } @else {
<ng-template [ngIf]="subdomain && enterpriseInfo"> <ng-template [ngIf]="subdomain && enterpriseInfo">
<div class="subdomain_container"> <div class="subdomain_container">
@ -49,7 +49,7 @@
</ng-template> </ng-template>
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState"> <ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
@if (enterpriseInfo?.header_img) { @if (enterpriseInfo?.header_img) {
<img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="36px"> <img [src]="enterpriseInfo?.header_img" [alt]="enterpriseInfo.title" height="36px">
} @else { } @else {
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" class="mempool-logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }"></app-svg-images> <app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" class="mempool-logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }"></app-svg-images>
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images> <app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>

View File

@ -4,7 +4,7 @@
<div class="nav-header"> <div class="nav-header">
@if (enterpriseInfo?.header_img) { @if (enterpriseInfo?.header_img) {
<a class="d-flex" [routerLink]="['/' | relativeUrl]"> <a class="d-flex" [routerLink]="['/' | relativeUrl]">
<img *ngIf="enterpriseInfo.header_img" [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="42px"> <img *ngIf="enterpriseInfo.header_img" [src]="enterpriseInfo?.header_img" [alt]="enterpriseInfo.title" height="42px">
</a> </a>
} @else if (enterpriseInfo?.img || enterpriseInfo?.imageMd5) { } @else if (enterpriseInfo?.img || enterpriseInfo?.imageMd5) {
<a [routerLink]="['/' | relativeUrl]"> <a [routerLink]="['/' | relativeUrl]">

View File

@ -158,6 +158,7 @@ export interface AddressTxSummary {
height: number; height: number;
time: number; time: number;
price?: number; price?: number;
tx_position?: number;
} }
export interface ChainStats { export interface ChainStats {

View File

@ -1,4 +1,4 @@
import { Block, Transaction } from "./electrs.interface"; import { AddressTxSummary, Block, Transaction } from "./electrs.interface";
export interface OptimizedMempoolStats { export interface OptimizedMempoolStats {
added: number; added: number;
@ -447,3 +447,9 @@ export interface TestMempoolAcceptResult {
}, },
['reject-reason']?: string, ['reject-reason']?: string,
} }
export interface WalletAddress {
address: string;
active: boolean;
transactions?: AddressTxSummary[];
}

View File

@ -36,6 +36,7 @@ export interface WebsocketResponse {
'track-rbf'?: string; 'track-rbf'?: string;
'track-rbf-summary'?: boolean; 'track-rbf-summary'?: boolean;
'track-accelerations'?: boolean; 'track-accelerations'?: boolean;
'track-wallet'?: string;
'watch-mempool'?: boolean; 'watch-mempool'?: boolean;
'refresh-blocks'?: boolean; 'refresh-blocks'?: boolean;
} }

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights,
RbfTree, BlockAudit, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo, RecentPeg, PegsVolume, AccelerationInfo, TestMempoolAcceptResult } from '../interfaces/node-api.interface'; RbfTree, BlockAudit, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo, RecentPeg, PegsVolume, AccelerationInfo, TestMempoolAcceptResult, WalletAddress } from '../interfaces/node-api.interface';
import { BehaviorSubject, Observable, catchError, filter, map, of, shareReplay, take, tap } from 'rxjs'; import { BehaviorSubject, Observable, catchError, filter, map, of, shareReplay, take, tap } from 'rxjs';
import { StateService } from './state.service'; import { StateService } from './state.service';
import { Transaction } from '../interfaces/electrs.interface'; import { Transaction } from '../interfaces/electrs.interface';
@ -504,6 +504,12 @@ export class ApiService {
); );
} }
getWallet$(walletName: string): Observable<Record<string, WalletAddress>> {
return this.httpClient.get<Record<string, WalletAddress>>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/wallet/${walletName}`
);
}
getAccelerationsByPool$(slug: string): Observable<AccelerationInfo[]> { getAccelerationsByPool$(slug: string): Observable<AccelerationInfo[]> {
return this.httpClient.get<AccelerationInfo[]>( return this.httpClient.get<AccelerationInfo[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/accelerations/pool/${slug}` this.apiBaseUrl + this.apiBasePath + `/api/v1/accelerations/pool/${slug}`

View File

@ -1,6 +1,6 @@
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core'; import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs'; import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface'; import { AddressTxSummary, Transaction } from '../interfaces/electrs.interface';
import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '../interfaces/websocket.interface'; import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '../interfaces/websocket.interface';
import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface'; import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router'; import { Router, NavigationStart } from '@angular/router';
@ -158,6 +158,7 @@ export class StateService {
mempoolRemovedTransactions$ = new Subject<Transaction>(); mempoolRemovedTransactions$ = new Subject<Transaction>();
multiAddressTransactions$ = new Subject<{ [address: string]: { mempool: Transaction[], confirmed: Transaction[], removed: Transaction[] }}>(); multiAddressTransactions$ = new Subject<{ [address: string]: { mempool: Transaction[], confirmed: Transaction[], removed: Transaction[] }}>();
blockTransactions$ = new Subject<Transaction>(); blockTransactions$ = new Subject<Transaction>();
walletTransactions$ = new Subject<Record<string, AddressTxSummary[]>>();
isLoadingWebSocket$ = new ReplaySubject<boolean>(1); isLoadingWebSocket$ = new ReplaySubject<boolean>(1);
isLoadingMempool$ = new BehaviorSubject<boolean>(true); isLoadingMempool$ = new BehaviorSubject<boolean>(true);
vbytesPerSecond$ = new ReplaySubject<number>(1); vbytesPerSecond$ = new ReplaySubject<number>(1);

View File

@ -34,6 +34,8 @@ export class WebsocketService {
private isTrackingAddress: string | false = false; private isTrackingAddress: string | false = false;
private isTrackingAddresses: string[] | false = false; private isTrackingAddresses: string[] | false = false;
private isTrackingAccelerations: boolean = false; private isTrackingAccelerations: boolean = false;
private isTrackingWallet: boolean = false;
private trackingWalletName: string;
private trackingMempoolBlock: number; private trackingMempoolBlock: number;
private latestGitCommit = ''; private latestGitCommit = '';
private onlineCheckTimeout: number; private onlineCheckTimeout: number;
@ -136,6 +138,9 @@ export class WebsocketService {
if (this.isTrackingAccelerations) { if (this.isTrackingAccelerations) {
this.startTrackAccelerations(); this.startTrackAccelerations();
} }
if (this.isTrackingWallet) {
this.startTrackingWallet(this.trackingWalletName);
}
this.stateService.connectionState$.next(2); this.stateService.connectionState$.next(2);
} }
@ -195,6 +200,18 @@ export class WebsocketService {
this.isTrackingAddresses = false; this.isTrackingAddresses = false;
} }
startTrackingWallet(walletName: string) {
this.websocketSubject.next({ 'track-wallet': walletName });
this.isTrackingWallet = true;
this.trackingWalletName = walletName;
}
stopTrackingWallet() {
this.websocketSubject.next({ 'track-wallet': 'stop' });
this.isTrackingWallet = false;
this.trackingWalletName = '';
}
startTrackAsset(asset: string) { startTrackAsset(asset: string) {
this.websocketSubject.next({ 'track-asset': asset }); this.websocketSubject.next({ 'track-asset': asset });
} }
@ -438,6 +455,10 @@ export class WebsocketService {
} }
} }
if (response['wallet-transactions']) {
this.stateService.walletTransactions$.next(response['wallet-transactions']);
}
if (response['accelerations']) { if (response['accelerations']) {
if (response['accelerations'].accelerations) { if (response['accelerations'].accelerations) {
this.stateService.accelerations$.next({ this.stateService.accelerations$.next({

View File

@ -5,7 +5,7 @@
<div class="col-md-12 branding mt-2"> <div class="col-md-12 branding mt-2">
<div class="main-logo" [class]="{'services': isServicesPage}"> <div class="main-logo" [class]="{'services': isServicesPage}">
@if (enterpriseInfo?.footer_img) { @if (enterpriseInfo?.footer_img) {
<img [src]="enterpriseInfo?.footer_img" alt="enterpriseInfo.title" height="60px" class="mr-3"> <img [src]="enterpriseInfo?.footer_img" [alt]="enterpriseInfo.title" height="60px" class="mr-3">
} @else { } @else {
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images> <app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126"></app-svg-images> <app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126"></app-svg-images>

View File

@ -23,7 +23,7 @@ export class FiatCurrencyPipe implements PipeTransform {
const digits = args[0] || 1; const digits = args[0] || 1;
const currency = args[1] || this.currency || 'USD'; const currency = args[1] || this.currency || 'USD';
if (num >= 1000) { if (Math.abs(num) >= 1000) {
return new Intl.NumberFormat(this.locale, { style: 'currency', currency, maximumFractionDigits: 0 }).format(num); return new Intl.NumberFormat(this.locale, { style: 'currency', currency, maximumFractionDigits: 0 }).format(num);
} else { } else {
return new Intl.NumberFormat(this.locale, { style: 'currency', currency }).format(num); return new Intl.NumberFormat(this.locale, { style: 'currency', currency }).format(num);