custom dashboard wallet widgets
This commit is contained in:
parent
4d06636d83
commit
64baade3b3
@ -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);
|
||||||
|
@ -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>
|
||||||
|
@ -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 {
|
||||||
|
@ -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">
|
||||||
|
@ -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;
|
||||||
|
@ -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">
|
||||||
|
@ -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) {
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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]">
|
||||||
|
@ -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 {
|
||||||
|
@ -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[];
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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}`
|
||||||
|
@ -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);
|
||||||
|
@ -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({
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user