Merge branch 'master' into hunicus/sv-about

This commit is contained in:
hunicus
2024-05-13 06:23:22 +09:00
committed by GitHub
174 changed files with 3055 additions and 940 deletions

View File

@@ -11,7 +11,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core';
import { combineLatest, BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs';
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs';
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service';
@@ -11,7 +11,7 @@ import { ServicesApiServices } from '../../../services/services-api.service';
styleUrls: ['./accelerations-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccelerationsListComponent implements OnInit {
export class AccelerationsListComponent implements OnInit, OnDestroy {
@Input() widget: boolean = false;
@Input() pending: boolean = false;
@Input() accelerations$: Observable<Acceleration[]>;
@@ -44,7 +44,10 @@ export class AccelerationsListComponent implements OnInit {
this.accelerationList$ = this.pageSubject.pipe(
switchMap((page) => {
const accelerationObservable$ = this.accelerations$ || (this.pending ? this.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistoryObserveResponse$({ page: page }));
const accelerationObservable$ = this.accelerations$ || (this.pending ? this.stateService.liveAccelerations$ : this.servicesApiService.getAccelerationHistoryObserveResponse$({ page: page }));
if (!this.accelerations$ && this.pending) {
this.websocketService.ensureTrackAccelerations();
}
return accelerationObservable$.pipe(
switchMap(response => {
let accelerations = response;
@@ -85,4 +88,8 @@ export class AccelerationsListComponent implements OnInit {
trackByBlock(index: number, block: BlockExtended): number {
return block.height;
}
ngOnDestroy(): void {
this.websocketService.stopTrackAccelerations();
}
}

View File

@@ -60,7 +60,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -1,10 +1,10 @@
import { ChangeDetectionStrategy, Component, HostListener, Inject, OnInit, PLATFORM_ID } from '@angular/core';
import { ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { SeoService } from '../../../services/seo.service';
import { OpenGraphService } from '../../../services/opengraph.service';
import { WebsocketService } from '../../../services/websocket.service';
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service';
import { Observable, catchError, combineLatest, distinctUntilChanged, interval, map, of, share, startWith, switchMap, tap } from 'rxjs';
import { Observable, Subscription, catchError, combineLatest, distinctUntilChanged, map, of, share, switchMap, tap } from 'rxjs';
import { Color } from '../../block-overview-graph/sprite-types';
import { hexToColor } from '../../block-overview-graph/utils';
import TxView from '../../block-overview-graph/tx-view';
@@ -28,7 +28,7 @@ interface AccelerationBlock extends BlockExtended {
styleUrls: ['./accelerator-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AcceleratorDashboardComponent implements OnInit {
export class AcceleratorDashboardComponent implements OnInit, OnDestroy {
blocks$: Observable<AccelerationBlock[]>;
accelerations$: Observable<Acceleration[]>;
pendingAccelerations$: Observable<Acceleration[]>;
@@ -39,6 +39,8 @@ export class AcceleratorDashboardComponent implements OnInit {
firstLoad = true;
timespan: '3d' | '1w' | '1m' = '1w';
accelerationDeltaSubscription: Subscription;
graphHeight: number = 300;
theme: ThemeService;
@@ -59,27 +61,28 @@ export class AcceleratorDashboardComponent implements OnInit {
ngOnInit(): void {
this.onResize();
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
this.websocketService.startTrackAccelerations();
this.pendingAccelerations$ = (this.stateService.isBrowser ? interval(30000) : of(null)).pipe(
startWith(true),
switchMap(() => {
return this.serviceApiServices.getAccelerations$().pipe(
catchError(() => {
return of([]);
}),
);
}),
tap(accelerations => {
if (!this.firstLoad && accelerations.some(acc => !this.seen.has(acc.txid))) {
this.audioService.playSound('bright-harmony');
}
for(const acc of accelerations) {
this.seen.add(acc.txid);
}
this.firstLoad = false;
}),
this.pendingAccelerations$ = this.stateService.liveAccelerations$.pipe(
share(),
);
this.accelerationDeltaSubscription = this.stateService.accelerations$.subscribe((delta) => {
if (!delta.reset) {
let hasNewAcceleration = false;
for (const acc of delta.added) {
if (!this.seen.has(acc.txid)) {
hasNewAcceleration = true;
}
this.seen.add(acc.txid);
}
for (const txid of delta.removed) {
this.seen.delete(txid);
}
if (hasNewAcceleration) {
this.audioService.playSound('bright-harmony');
}
}
});
this.accelerations$ = this.stateService.chainTip$.pipe(
distinctUntilChanged(),
@@ -145,7 +148,7 @@ export class AcceleratorDashboardComponent implements OnInit {
} else {
const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1;
return this.theme.theme === 'contrast' ? contrastColors[feeLevelIndex] || contrastColors[contrastColors.length - 1] : normalColors[feeLevelIndex] || normalColors[normalColors.length - 1];
return this.theme.theme === 'contrast' || this.theme.theme === 'bukele' ? contrastColors[feeLevelIndex] || contrastColors[contrastColors.length - 1] : normalColors[feeLevelIndex] || normalColors[normalColors.length - 1];
}
}
@@ -154,6 +157,11 @@ export class AcceleratorDashboardComponent implements OnInit {
return false;
}
ngOnDestroy(): void {
this.accelerationDeltaSubscription.unsubscribe();
this.websocketService.stopTrackAccelerations();
}
@HostListener('window:resize', ['$event'])
onResize(): void {
if (window.innerWidth >= 992) {

View File

@@ -2,7 +2,8 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core
import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { Acceleration } from '../../../interfaces/node-api.interface';
import { ServicesApiServices } from '../../../services/services-api.service';
import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service';
@Component({
selector: 'app-pending-stats',
@@ -15,11 +16,12 @@ export class PendingStatsComponent implements OnInit {
public accelerationStats$: Observable<any>;
constructor(
private servicesApiService: ServicesApiServices,
private stateService: StateService,
private websocketService: WebsocketService,
) { }
ngOnInit(): void {
this.accelerationStats$ = (this.accelerations$ || this.servicesApiService.getAccelerations$()).pipe(
this.accelerationStats$ = (this.accelerations$ || this.stateService.liveAccelerations$).pipe(
switchMap(accelerations => {
let totalAccelerations = 0;
let totalFeeDelta = 0;

View File

@@ -1,12 +1,6 @@
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
<div [class.full-container]="!widget">
<div *ngIf="!widget" class="card-header mb-0 mb-md-2">
<div class="d-flex d-md-block align-items-baseline">
<span i18n="address.balance-history">Balance History</span>
</div>
</div>
<ng-container *ngIf="!error">
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">

View File

@@ -11,7 +11,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;
@@ -45,23 +46,8 @@
display: flex;
flex: 1;
width: 100%;
padding-bottom: 20px;
padding-bottom: 10px;
padding-right: 10px;
@media (max-width: 992px) {
padding-bottom: 25px;
}
@media (max-width: 829px) {
padding-bottom: 50px;
}
@media (max-width: 767px) {
padding-bottom: 25px;
}
@media (max-width: 629px) {
padding-bottom: 55px;
}
@media (max-width: 567px) {
padding-bottom: 55px;
}
}
.chart-widget {
width: 100%;

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, SimpleChanges } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
import { echarts, EChartsOption } from '../../graphs/echarts';
import { Observable, of } from 'rxjs';
import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service';
@@ -32,7 +32,7 @@ const periodSeconds = {
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddressGraphComponent implements OnChanges {
export class AddressGraphComponent implements OnChanges, OnDestroy {
@Input() address: string;
@Input() isPubkey: boolean = false;
@Input() stats: ChainStats;
@@ -46,6 +46,9 @@ export class AddressGraphComponent implements OnChanges {
data: any[] = [];
hoverData: any[] = [];
subscription: Subscription;
redraw$: BehaviorSubject<boolean> = new BehaviorSubject(false);
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg',
@@ -70,24 +73,38 @@ export class AddressGraphComponent implements OnChanges {
if (!this.address || !this.stats) {
return;
}
(this.addressSummary$ || (this.isPubkey
? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac')
: this.electrsApiService.getAddressSummary$(this.address)).pipe(
catchError(e => {
this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`;
return of(null);
}),
)).subscribe(addressSummary => {
if (addressSummary) {
this.error = null;
this.prepareChartOptions(addressSummary);
if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
if (this.subscription) {
this.subscription.unsubscribe();
}
this.isLoading = false;
this.cd.markForCheck();
});
this.subscription = combineLatest([
this.redraw$,
(this.addressSummary$ || (this.isPubkey
? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac')
: this.electrsApiService.getAddressSummary$(this.address)).pipe(
catchError(e => {
this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`;
return of(null);
}),
))
]).subscribe(([redraw, addressSummary]) => {
if (addressSummary) {
this.error = null;
this.prepareChartOptions(addressSummary);
}
this.isLoading = false;
this.cd.markForCheck();
});
} else {
// re-trigger subscription
this.redraw$.next(true);
}
}
prepareChartOptions(summary): void {
if (!summary || !this.stats) {
return;
}
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum);
this.data = summary.map(d => {
const balance = total;
@@ -104,8 +121,8 @@ export class AddressGraphComponent implements OnChanges {
);
}
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] || d.value[1])), 0);
const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] || d.value[1])), maxValue);
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0);
const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] ?? d.value[1])), maxValue);
this.chartOptions = {
color: [
@@ -230,6 +247,12 @@ export class AddressGraphComponent implements OnChanges {
this.chartInstance.on('click', 'series', this.onChartClick.bind(this));
}
ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
isMobile() {
return (window.innerWidth <= 767.98);
}

View File

@@ -3,7 +3,7 @@
}
.qr-wrapper {
background-color: var(--fg);
background-color: #fff;
padding: 10px;
padding-bottom: 5px;
display: inline-block;

View File

@@ -53,10 +53,20 @@
<ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && transactions && transactions.length > 2">
<br>
<div class="title-tx">
<h2 class="text-left" i18n="address.balance-history">Balance History</h2>
</div>
<div class="box">
<div class="widget-toggler" *ngIf="showBalancePeriod()">
<a href="" (click)="setBalancePeriod('all')" class="toggler-option"
[ngClass]="{'inactive': balancePeriod === 'all'}"><small i18n="all">all</small></a>
<span style="color: var(--transparent-fg); font-size: 8px"> | </span>
<a href="" (click)="setBalancePeriod('1m')" class="toggler-option"
[ngClass]="{'inactive': balancePeriod === '1m'}"><small i18n="recent">recent</small></a>
</div>
<div class="row">
<div class="col-md">
<app-address-graph [address]="addressString" [isPubkey]="address?.is_pubkey" [stats]="address.chain_stats" />
<app-address-graph [address]="addressString" [isPubkey]="address?.is_pubkey" [stats]="address.chain_stats" [period]="balancePeriod" />
</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
.qr-wrapper {
background-color: var(--fg);
background-color: #fff;
padding: 10px;
padding-bottom: 5px;
display: inline-block;
@@ -109,3 +109,19 @@ h1 {
flex-grow: 0.5;
}
}
.widget-toggler {
font-size: 12px;
position: absolute;
top: -20px;
right: 3px;
text-align: right;
}
.toggler-option {
text-decoration: none;
}
.inactive {
color: var(--transparent-fg);
}

View File

@@ -38,6 +38,8 @@ export class AddressComponent implements OnInit, OnDestroy {
txCount = 0;
received = 0;
sent = 0;
now = Date.now() / 1000;
balancePeriod: 'all' | '1m' = 'all';
private tempTransactions: Transaction[];
private timeTxIndexes: number[];
@@ -174,6 +176,10 @@ export class AddressComponent implements OnInit, OnDestroy {
this.transactions = this.tempTransactions;
this.isLoadingTransactions = false;
if (!this.showBalancePeriod()) {
this.setBalancePeriod('all');
}
},
(error) => {
console.log(error);
@@ -296,6 +302,18 @@ export class AddressComponent implements OnInit, OnDestroy {
this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count;
}
setBalancePeriod(period: 'all' | '1m'): boolean {
this.balancePeriod = period;
return false;
}
showBalancePeriod(): boolean {
return this.transactions?.length && (
!this.transactions[0].status?.confirmed
|| this.transactions[0].status.block_time > (this.now - (60 * 60 * 24 * 30))
);
}
ngOnDestroy() {
this.mainSubscription.unsubscribe();
this.mempoolTxSubscription.unsubscribe();

View File

@@ -43,5 +43,6 @@
<ng-template [ngIf]="network === 'liquid' && !forceBtc">L-</ng-template>
<ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template>
<ng-template [ngIf]="network === 'testnet'">t</ng-template>
<ng-template [ngIf]="network === 'testnet4'">t</ng-template>
<ng-template [ngIf]="network === 'signet'">s</ng-template>
</ng-template>

View File

@@ -1,5 +1,5 @@
.qr-wrapper {
background-color: var(--fg);
background-color: #fff;
padding: 10px;
padding-bottom: 5px;
display: inline-block;

View File

@@ -57,8 +57,9 @@ export class BalanceWidgetComponent implements OnInit, OnChanges {
calculateStats(summary: AddressTxSummary[]): void {
let weekTotal = 0;
let monthTotal = 0;
const weekAgo = (Date.now() / 1000) - (60 * 60 * 24 * 7);
const monthAgo = (Date.now() / 1000) - (60 * 60 * 24 * 30);
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;
for (let i = 0; i < summary.length && summary[i].time >= monthAgo; i++) {
monthTotal += summary[i].value;
if (summary[i].time >= weekAgo) {

View File

@@ -11,7 +11,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -11,7 +11,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -11,7 +11,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -81,6 +81,20 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
tooltipPosition: Position;
readyNextFrame = false;
lastUpdate: number = 0;
pendingUpdate: {
count: number,
add: { [txid: string]: TransactionStripped },
remove: { [txid: string]: string },
change: { [txid: string]: { txid: string, rate: number | undefined, acc: boolean | undefined } },
direction?: string,
} = {
count: 0,
add: {},
remove: {},
change: {},
direction: 'left',
};
searchText: string;
searchSubscription: Subscription;
@@ -176,6 +190,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
destroy(): void {
if (this.scene) {
this.scene.destroy();
this.clearUpdateQueue();
this.start();
}
}
@@ -188,6 +203,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
this.filtersAvailable = filtersAvailable;
if (this.scene) {
this.clearUpdateQueue();
this.scene.setup(transactions);
this.readyNextFrame = true;
this.start();
@@ -197,6 +213,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
enter(transactions: TransactionStripped[], direction: string): void {
if (this.scene) {
this.clearUpdateQueue();
this.scene.enter(transactions, direction);
this.start();
this.updateSearchHighlight();
@@ -205,6 +222,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
exit(direction: string): void {
if (this.scene) {
this.clearUpdateQueue();
this.scene.exit(direction);
this.start();
this.updateSearchHighlight();
@@ -213,13 +231,67 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void {
if (this.scene) {
this.clearUpdateQueue();
this.scene.replace(transactions || [], direction, sort, startTime);
this.start();
this.updateSearchHighlight();
}
}
// collates deferred updates into a set of consistent pending changes
queueUpdate(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void {
for (const tx of add) {
this.pendingUpdate.add[tx.txid] = tx;
delete this.pendingUpdate.remove[tx.txid];
delete this.pendingUpdate.change[tx.txid];
}
for (const txid of remove) {
delete this.pendingUpdate.add[txid];
this.pendingUpdate.remove[txid] = txid;
delete this.pendingUpdate.change[txid];
}
for (const tx of change) {
if (this.pendingUpdate.add[tx.txid]) {
this.pendingUpdate.add[tx.txid].rate = tx.rate;
this.pendingUpdate.add[tx.txid].acc = tx.acc;
} else {
this.pendingUpdate.change[tx.txid] = tx;
}
}
this.pendingUpdate.direction = direction;
this.pendingUpdate.count++;
}
deferredUpdate(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void {
this.queueUpdate(add, remove, change, direction);
this.applyQueuedUpdates();
}
applyQueuedUpdates(): void {
if (this.pendingUpdate.count && performance.now() > (this.lastUpdate + this.animationDuration)) {
this.applyUpdate(Object.values(this.pendingUpdate.add), Object.values(this.pendingUpdate.remove), Object.values(this.pendingUpdate.change), this.pendingUpdate.direction);
this.clearUpdateQueue();
}
}
clearUpdateQueue(): void {
this.pendingUpdate = {
count: 0,
add: {},
remove: {},
change: {},
};
this.lastUpdate = performance.now();
}
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
// merge any pending changes into this update
this.queueUpdate(add, remove, change);
this.applyUpdate(Object.values(this.pendingUpdate.add), Object.values(this.pendingUpdate.remove), Object.values(this.pendingUpdate.change), direction, resetLayout);
this.clearUpdateQueue();
}
applyUpdate(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
if (this.scene) {
add = add.filter(tx => !this.scene.txs[tx.txid]);
remove = remove.filter(txid => this.scene.txs[txid]);
@@ -230,6 +302,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
this.scene.update(add, remove, change, direction, resetLayout);
this.start();
this.lastUpdate = performance.now();
this.updateSearchHighlight();
}
}
@@ -370,6 +443,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
if (!now) {
now = performance.now();
}
this.applyQueuedUpdates();
// skip re-render if there's no change to the scene
if (this.scene && this.gl) {
/* SET UP SHADER UNIFORMS */
@@ -577,13 +651,13 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
getFilterColorFunction(flags: bigint, gradient: 'fee' | 'age'): ((tx: TxView) => Color) {
return (tx: TxView) => {
if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) {
if (this.themeService.theme !== 'contrast') {
if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') {
return (gradient === 'age') ? ageColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000)) : defaultColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000));
} else {
return (gradient === 'age') ? ageColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000)) : contrastColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000));
}
} else {
if (this.themeService.theme !== 'contrast') {
if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') {
return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : defaultColorFunction(
tx,
defaultColors.unmatchedfee,

View File

@@ -13,7 +13,7 @@ export default class BlockScene {
theme: ThemeService;
orientation: string;
flip: boolean;
animationDuration: number = 900;
animationDuration: number = 1000;
configAnimationOffset: number | null;
animationOffset: number;
highlightingEnabled: boolean;
@@ -69,7 +69,7 @@ export default class BlockScene {
}
setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void {
this.theme.theme === 'contrast' ? this.getColor = colorFunction || contrastColorFunction : this.getColor = colorFunction || defaultColorFunction;
this.theme.theme === 'contrast' || this.theme.theme === 'bukele' ? this.getColor = colorFunction || contrastColorFunction : this.getColor = colorFunction || defaultColorFunction;
this.updateAllColors();
}
@@ -179,7 +179,7 @@ export default class BlockScene {
removed.forEach(tx => {
tx.destroy();
});
}, 1000);
}, (startTime - performance.now()) + this.animationDuration + 1000);
if (resetLayout) {
add.forEach(tx => {
@@ -239,14 +239,14 @@ export default class BlockScene {
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
): void {
this.animationDuration = animationDuration || 1000;
this.animationDuration = animationDuration || this.animationDuration || 1000;
this.configAnimationOffset = animationOffset;
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
this.orientation = orientation;
this.flip = flip;
this.vertexArray = vertexArray;
this.highlightingEnabled = highlighting;
theme.theme === 'contrast' ? this.getColor = colorFunction || contrastColorFunction : this.getColor = colorFunction || defaultColorFunction;
theme.theme === 'contrast' || theme.theme === 'bukele' ? this.getColor = colorFunction || contrastColorFunction : this.getColor = colorFunction || defaultColorFunction;
this.theme = theme;
this.scene = {

View File

@@ -177,7 +177,7 @@ export function ageColorFunction(
return auditColors.accelerated;
}
const color = theme !== 'contrast' ? defaultColorFunction(tx, colors, auditColors, relativeTime) : contrastColorFunction(tx, colors, auditColors, relativeTime);
const color = theme !== 'contrast' && theme !== 'bukele' ? defaultColorFunction(tx, colors, auditColors, relativeTime) : contrastColorFunction(tx, colors, auditColors, relativeTime);
const ageLevel = (!tx.time ? 0 : (0.8 * Math.tanh((1 / 15) * Math.log2((Math.max(1, 0.6 * ((relativeTime - tx.time) - 60)))))));
return {

View File

@@ -1,6 +1,6 @@
.block-overview-tooltip {
position: absolute;
background: rgba(#11131f, 0.95);
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
color: var(--tooltip-grey);
@@ -30,7 +30,7 @@ th, td {
}
.badge.badge-accelerated {
background-color: var(--tertiary);
background-color: #653b9c;
box-shadow: #ad7de57f 0px 0px 12px -2px;
color: white;
animation: acceleratePulse 1s infinite;
@@ -71,7 +71,7 @@ th, td {
}
@keyframes acceleratePulse {
0% { background-color: var(--tertiary); box-shadow: #ad7de57f 0px 0px 12px -2px; }
0% { background-color: #653b9c; box-shadow: #ad7de57f 0px 0px 12px -2px; }
50% { background-color: #8457bb; box-shadow: #ad7de5 0px 0px 18px -2px;}
100% { background-color: var(--tertiary); box-shadow: #ad7de57f 0px 0px 12px -2px; }
100% { background-color: #653b9c; box-shadow: #ad7de57f 0px 0px 12px -2px; }
}

View File

@@ -11,7 +11,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -11,7 +11,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -63,7 +63,7 @@
.fee-span {
font-size: 11px;
margin-bottom: 5px;
color: #fff000;
color: var(--yellow);
}
.transaction-count {
@@ -130,7 +130,7 @@
height: 0;
border-left: calc(var(--block-size) * 0.3) solid transparent;
border-right: calc(var(--block-size) * 0.3) solid transparent;
border-bottom: calc(var(--block-size) * 0.3) solid #FFF;
border-bottom: calc(var(--block-size) * 0.3) solid var(--fg);
}
.flashing {

View File

@@ -70,6 +70,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
liquid: ['var(--liquid)', 'var(--testnet-alt)'],
'liquidtestnet': ['var(--liquidtestnet)', 'var(--liquidtestnet-alt)'],
testnet: ['var(--testnet)', 'var(--testnet-alt)'],
testnet4: ['var(--testnet)', 'var(--testnet-alt)'],
signet: ['var(--signet)', 'var(--signet-alt)'],
};
@@ -349,7 +350,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
return {
left: addLeft + this.blockOffset * index + 'px',
background: `repeating-linear-gradient(
#2d3348,
var(--secondary),
var(--secondary) ${greenBackgroundHeight}%,
${this.gradientColors[this.network][0]} ${Math.max(greenBackgroundHeight, 0)}%,
${this.gradientColors[this.network][1]} 100%
@@ -361,7 +362,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
convertStyleForLoadingBlock(style) {
return {
...style,
background: "#2d3348",
background: "var(--secondary)",
};
}
@@ -370,7 +371,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
return {
left: addLeft + (this.blockOffset * index) + 'px',
background: "#2d3348",
background: "var(--secondary)",
};
}

View File

@@ -54,7 +54,7 @@
}
.time-toggle {
color: white;
color: var(--fg);
font-size: 0.8rem;
position: absolute;
bottom: -1.8em;
@@ -68,7 +68,7 @@
}
.block-display-toggle {
color: white;
color: var(--fg);
font-size: 0.8rem;
position: absolute;
bottom: 15.8em;

View File

@@ -55,7 +55,7 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
firstValueFrom(this.stateService.chainTip$).then(() => {
this.loadingTip = false;
});
this.blockDisplayMode = this.StorageService.getValue('block-display-mode-preference') as 'size' | 'fees' || 'size';
this.blockDisplayMode = this.StorageService.getValue('block-display-mode-preference') as 'size' | 'fees' || 'fees';
}
ngOnDestroy(): void {

View File

@@ -32,11 +32,12 @@ export class ClockComponent implements OnInit {
limitHeight: number;
gradientColors = {
'': ['#9339f4', '#105fb0'],
liquid: ['#116761', '#183550'],
'liquidtestnet': ['#494a4a', '#272e46'],
testnet: ['#1d486f', '#183550'],
signet: ['#6f1d5d', '#471850'],
'': ['var(--mainnet-alt)', 'var(--primary)'],
liquid: ['var(--liquid)', 'var(--testnet-alt)'],
'liquidtestnet': ['var(--liquidtestnet)', 'var(--liquidtestnet-alt)'],
testnet: ['var(--testnet)', 'var(--testnet-alt)'],
testnet4: ['var(--testnet)', 'var(--testnet-alt)'],
signet: ['var(--signet)', 'var(--signet-alt)'],
};
constructor(
@@ -99,8 +100,8 @@ export class ClockComponent implements OnInit {
return {
background: `repeating-linear-gradient(
#2d3348,
#2d3348 ${greenBackgroundHeight}%,
var(--secondary),
var(--secondary) ${greenBackgroundHeight}%,
${this.gradientColors[''][0]} ${Math.max(greenBackgroundHeight, 0)}%,
${this.gradientColors[''][1]} 100%
)`,

View File

@@ -4,7 +4,7 @@
@for (widget of widgets; track widget.component) {
@switch (widget.component) {
@case ('fees') {
<div class="col card-wrapper">
<div class="col card-wrapper" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
<div class="card">
<div class="card-body less-padding">
@@ -14,12 +14,12 @@
</div>
}
@case ('difficulty') {
<div class="col">
<div class="col" [style.order]="isMobile && widget.mobileOrder || 8">
<app-difficulty></app-difficulty>
</div>
}
@case ('goggles') {
<div class="col">
<div class="col" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card graph-card">
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
<a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/mempool-block/0' | relativeUrl]">
@@ -48,7 +48,7 @@
</div>
}
@case ('incoming') {
<div class="col">
<div class="col" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card graph-card">
<div class="card-body">
<ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
@@ -93,7 +93,7 @@
</ng-template>
}
@case ('replacements') {
<div class="col" style="max-height: 410px">
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card">
<div class="card-body">
<a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
@@ -140,7 +140,7 @@
</ng-template>
}
@case ('blocks') {
<div class="col" style="max-height: 410px">
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card">
<div class="card-body">
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
@@ -184,7 +184,7 @@
</ng-template>
}
@case ('transactions') {
<div class="col" style="max-height: 410px">
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card">
<div class="card-body">
<h5 class="card-title" i18n="dashboard.recent-transactions">Recent Transactions</h5>
@@ -224,13 +224,13 @@
</ng-template>
}
@case ('balance') {
<div class="col card-wrapper">
<div class="col card-wrapper" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="main-title" i18n="dashboard.treasury">Treasury</div>
<app-balance-widget [address]="widget.props.address" [addressSummary$]="addressSummary$" [addressInfo]="address"></app-balance-widget>
</div>
}
@case ('address') {
<div class="col" style="max-height: 410px">
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card graph-card">
<div class="card-body">
<a class="title-link" href="" [routerLink]="[('/address/' + widget.props.address) | relativeUrl]">
@@ -238,13 +238,13 @@
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
<app-address-graph [address]="widget.props.address" [addressSummary$]="addressSummary$" [period]="widget.props.period || 'all'" [stats]="address?.chain_stats" [widget]="true" [height]="graphHeight"></app-address-graph>
<app-address-graph [address]="widget.props.address" [addressSummary$]="addressSummary$" [period]="widget.props.period || 'all'" [stats]="address ? address.chain_stats : null" [widget]="true" [height]="graphHeight"></app-address-graph>
</div>
</div>
</div>
}
@case ('addressTransactions') {
<div class="col" style="max-height: 410px">
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card">
<div class="card-body">
<a class="title-link" href="" [routerLink]="[('/address/' + widget.props.address) | relativeUrl]">
@@ -257,6 +257,22 @@
</div>
</div>
}
@case ('twitter') {
<div class="col" style="min-height:410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card graph-card">
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2 d-flex flex-column">
<a class="title-link" [href]="'https://x.com/' + widget.props?.handle" target="_blank">
<h5 class="card-title d-inline" i18n="dashboard.x-timeline">X Timeline</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
@defer {
<app-twitter-widget [handle]="widget.props?.handle" style="flex-grow: 1"></app-twitter-widget>
}
</div>
</div>
</div>
}
}
}
</div>

View File

@@ -1,4 +1,4 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { combineLatest, merge, Observable, of, Subject, Subscription } from 'rxjs';
import { catchError, filter, map, scan, share, shareReplay, startWith, switchMap, tap } from 'rxjs/operators';
import { BlockExtended, OptimizedMempoolStats, TransactionStripped } from '../../interfaces/node-api.interface';
@@ -57,6 +57,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
incomingGraphHeight: number = 300;
graphHeight: number = 300;
webGlEnabled = true;
isMobile: boolean = window.innerWidth <= 767.98;
widgets;
@@ -85,6 +86,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
private electrsApiService: ElectrsApiService,
private websocketService: WebsocketService,
private seoService: SeoService,
private cd: ChangeDetectorRef,
@Inject(PLATFORM_ID) private platformId: Object,
) {
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
@@ -230,8 +232,10 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
this.stateService.live2Chart$
.pipe(
scan((acc, stats) => {
const now = Date.now() / 1000;
const start = now - (2 * 60 * 60);
acc.unshift(stats);
acc = acc.slice(0, 120);
acc = acc.filter(p => p.added >= start);
return acc;
}, (mempoolStats || []))
),
@@ -283,8 +287,8 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
startAddressSubscription(): void {
if (this.stateService.env.customize && this.stateService.env.customize.dashboard.widgets.some(w => w.props?.address)) {
const address = this.stateService.env.customize.dashboard.widgets.find(w => w.props?.address).props.address;
const addressString = (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(address)) ? address.toLowerCase() : address;
let addressString = this.stateService.env.customize.dashboard.widgets.find(w => w.props?.address).props.address;
addressString = (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(addressString)) ? addressString.toLowerCase() : addressString;
this.addressSubscription = (
addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
@@ -299,6 +303,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
).subscribe((address: Address) => {
this.websocketService.startTrackAddress(address.address);
this.address = address;
this.cd.markForCheck();
});
this.addressSummary$ = (
@@ -368,5 +373,6 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
this.goggleResolution = 86;
this.graphHeight = 310;
}
this.isMobile = window.innerWidth <= 767.98;
}
}

View File

@@ -119,7 +119,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -1,9 +1,9 @@
.difficulty-tooltip {
position: fixed;
background: rgba(#11131f, 0.95);
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
color: #b1b1b1;
color: var(--tooltip-grey);
padding: 10px 15px;
text-align: left;
pointer-events: none;

View File

@@ -15,8 +15,8 @@
<svg #epochSvg class="epoch-blocks" height="22px" width="100%" viewBox="0 0 224 9" shape-rendering="crispEdges" preserveAspectRatio="none">
<defs>
<linearGradient id="diff-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#105fb0" />
<stop offset="100%" stop-color="#9339f4" />
<stop offset="0%" stop-color="var(--primary)" />
<stop offset="100%" stop-color="var(--mainnet-alt)" />
</linearGradient>
<linearGradient id="diff-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#2486eb" />

View File

@@ -128,7 +128,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;
@@ -223,7 +224,7 @@
height: 100%;
}
.background {
background: linear-gradient(to right, var(--primary), #9339f4);
background: linear-gradient(to right, var(--primary), var(--mainnet-alt));
left: 0;
right: 0;
}

View File

@@ -79,7 +79,7 @@
display: flex;
flex-direction: row;
transition: background-color 1s;
color: var(--color-fg);
color: #fff;
&.priority {
@media (767px < width < 992px), (width < 576px) {
width: 100%;

View File

@@ -16,8 +16,8 @@ export class FeesBoxComponent implements OnInit, OnDestroy {
isLoading$: Observable<boolean>;
recommendedFees$: Observable<Recommendedfees>;
themeSubscription: Subscription;
gradient = 'linear-gradient(to right, #2e324e, #2e324e)';
noPriority = '#2e324e';
gradient = 'linear-gradient(to right, var(--skeleton-bg), var(--skeleton-bg))';
noPriority = 'var(--skeleton-bg)';
fees: Recommendedfees;
constructor(

View File

@@ -11,7 +11,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -11,7 +11,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -66,7 +66,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
if (!this.data) {
return;
}
this.windowPreference = this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference');
this.windowPreference = (this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference')) || '2h';
const windowSize = Math.max(10, Math.floor(this.data.series[0].length / 8));
this.MA = this.calculateMA(this.data.series[0], windowSize);
if (this.outlierCappingEnabled === true) {
@@ -216,22 +216,19 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
type: 'line',
},
formatter: (params: any) => {
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, params[0].axisValue);
const bestItem = params.reduce((best, item) => {
return (item.seriesName === 'data' && (!best || best.value[1] < item.value[1])) ? item : best;
}, null);
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, bestItem.axisValue);
const colorSpan = (color: string) => `<span class="indicator" style="background-color: ` + color + `"></span>`;
let itemFormatted = '<div class="title">' + axisValueLabel + '</div>';
params.map((item: any, index: number) => {
//Do no include MA in tooltip legend!
if (item.seriesName !== 'MA') {
if (index < 26) {
itemFormatted += `<div class="item">
<div class="indicator-container">${colorSpan(item.color)}</div>
if (bestItem) {
itemFormatted += `<div class="item">
<div class="indicator-container">${colorSpan(bestItem.color)}</div>
<div class="grow"></div>
<div class="value">${formatNumber(item.value[1], this.locale, '1.0-0')}<span class="symbol">vB/s</span></div>
<div class="value">${formatNumber(bestItem.value[1], this.locale, '1.0-0')}<span class="symbol">vB/s</span></div>
</div>`;
}
}
});
}
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}"
style="width: ${(this.windowPreference === '2h' || this.template === 'widget') ? '125px' : '215px'}">${itemFormatted}</div>`;
}

View File

@@ -51,7 +51,8 @@
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['signet'] || '/signet')" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet3</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet4'] || '/testnet4')" ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4 <span class="badge badge-pill badge-warning beta-network" i18n="beta">beta</span></a>
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
<a ngbDropdownItem class="liquid mr-1" [class.active]="network.val === 'liquid'" [routerLink]="networkPaths['liquid'] || '/'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
<a ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" [routerLink]="networkPaths['liquidtestnet'] || '/testnet'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>

View File

@@ -4,6 +4,7 @@
top: 0;
width: 100%;
z-index: 100;
background-color: var(--bg);
}
li.nav-item.active {
@@ -17,7 +18,7 @@ fa-icon {
.navbar {
z-index: 100;
min-height: 64px;
background-color: var(--bg);
background-color: var(--nav-bg);
}
li.nav-item {
@@ -48,7 +49,7 @@ li.nav-item {
}
.navbar-nav {
background: var(--navbar-bg);
background: var(--nav-bg);
bottom: 0;
box-shadow: 0px 0px 15px 0px #000;
flex-direction: row;
@@ -169,4 +170,8 @@ nav {
margin-left: 5px;
margin-right: 0px;
}
}
.beta-network {
font-size: 8px;
}

View File

@@ -6,7 +6,7 @@
<img [src]="enterpriseInfo.img" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
}
@if (enterpriseInfo?.header_img) {
<img *ngIf="enterpriseInfo.header_img" [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="36px">
<img *ngIf="enterpriseInfo.header_img" [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="60px" class="mr-3">
} @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="officialMempoolSpace" style="width: 200px; height: 50px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
@@ -15,7 +15,8 @@
<div [ngSwitch]="network.val">
<span *ngSwitchCase="'signet'" class="network signet"><app-svg-images name="signet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Signet</span>
<span *ngSwitchCase="'testnet'" class="network testnet"><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet</span>
<span *ngSwitchCase="'testnet'" class="network testnet"><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet3</span>
<span *ngSwitchCase="'testnet4'" class="network testnet"><app-svg-images name="testnet4" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet4</span>
<span *ngSwitchCase="'liquid'" class="network liquid"><app-svg-images name="liquid" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Mainnet</span>
<span *ngSwitchCase="'liquidtestnet'" class="network liquidtestnet"><app-svg-images name="liquidtestnet" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet</span>
<span *ngSwitchDefault class="network mainnet"><app-svg-images name="bitcoin" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Mainnet</span>

View File

@@ -5,6 +5,7 @@
max-width: 1200px;
max-height: 600px;
padding-top: 80px;
background: var(--nav-bg);
header {
position: absolute;
@@ -18,7 +19,7 @@
flex-direction: row;
justify-content: space-between;
align-items: center;
background: var(--stat-box-bg);
background: var(--nav-bg);
text-align: start;
font-size: 1.8em;
}

View File

@@ -17,16 +17,16 @@
<!-- Large screen -->
<a class="navbar-brand d-none d-md-flex" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
<ng-template [ngIf]="subdomain && enterpriseInfo">
<div class="subdomain_container">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
</div>
<div class="vertical-line"></div>
</ng-template>
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
@if (enterpriseInfo?.header_img) {
<img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="36px">
<img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="48px" class="mr-3">
} @else {
<ng-template [ngIf]="subdomain && enterpriseInfo">
<div class="subdomain_container">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
</div>
<div class="vertical-line"></div>
</ng-template>
<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>
}
@@ -38,34 +38,39 @@
</a>
<!-- 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)">
<ng-template [ngIf]="subdomain && enterpriseInfo">
<div class="subdomain_container">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
</div>
<div class="vertical-line"></div>
</ng-template>
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
@if (enterpriseInfo?.header_img) {
<img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="36px">
} @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="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
}
<div class="connection-badge">
<div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>
</div>
</ng-container>
@if (enterpriseInfo?.header_img) {
<img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="42px">
} @else {
<ng-template [ngIf]="subdomain && enterpriseInfo">
<div class="subdomain_container">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
</div>
<div class="vertical-line"></div>
</ng-template>
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
@if (enterpriseInfo?.header_img) {
<img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="36px">
} @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="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
}
<div class="connection-badge">
<div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>
</div>
</ng-container>
}
</a>
<div (window:resize)="onResize()" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.LIQUID_TESTNET_ENABLED">
<div (window:resize)="onResize()" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.TESTNET4_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.LIQUID_TESTNET_ENABLED">
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true">
<app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" [name]="network.val === '' ? 'bitcoin' : network.val" width="20" height="20" viewBox="0 0 65 65"></app-svg-images>
</button>
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
<a ngbDropdownItem class="mainnet" [routerLink]="networkPaths['mainnet'] || '/'"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
<a ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" [routerLink]="networkPaths['signet'] || '/signet'"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
<a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" [routerLink]="networkPaths['testnet'] || '/testnet'"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
<a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" [routerLink]="networkPaths['testnet'] || '/testnet'"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet3</a>
<a ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet" [class.active]="network.val === 'testnet4'" [routerLink]="networkPaths['testnet4'] || '/testnet4'"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4 <span class="badge badge-pill badge-warning beta-network" i18n="beta">beta</span></a>
<h6 *ngIf="env.LIQUID_ENABLED" class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquid'] || '')" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquidtestnet'] || '/testnet')" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquid'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
@@ -87,7 +92,7 @@
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD">
<a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="mining.mining-dashboard" title="Mining Dashboard"></fa-icon></a>
</li>
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-lightning" *ngIf="stateService.env.LIGHTNING">
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-lightning" *ngIf="stateService.env.LIGHTNING && lightningNetworks.includes(stateService.network)">
<a class="nav-link" [routerLink]="['/lightning' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'bolt']" [fixedWidth]="true" i18n-title="master-page.lightning" title="Lightning Explorer"></fa-icon>
</a>
</li>
@@ -114,7 +119,7 @@
<div class="empty-sidenav"><!-- empty sidenav needed to push footer down the screen --></div>
<div class="flex-grow-1 d-flex flex-column">
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert>
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'testnet4' || network.val === 'signet'"></app-testnet-alert>
<main style="min-width: 375px; max-width: 100vw">
<router-outlet></router-outlet>

View File

@@ -3,6 +3,7 @@
position: -webkit-sticky;
top: 0;
width: 100%;
background-color: var(--bg);
z-index: 100;
}
@@ -18,7 +19,7 @@ fa-icon {
z-index: 100;
min-height: 64px;
width: 100%;
background-color: var(--bg);
background-color: var(--nav-bg);
}
li.nav-item {
@@ -59,7 +60,7 @@ li.nav-item {
}
.navbar-nav {
background: var(--navbar-bg);
background: var(--nav-bg);
bottom: 0;
box-shadow: 0px 0px 15px 0px #000;
flex-direction: row;
@@ -243,6 +244,10 @@ nav {
}
}
.beta-network {
font-size: 8px;
}
.current-network-svg {
width: 20px;
height: 20px;

View File

@@ -27,6 +27,7 @@ export class MasterPageComponent implements OnInit, OnDestroy {
subdomain = '';
networkPaths: { [network: string]: string };
networkPaths$: Observable<Record<string, string>>;
lightningNetworks = ['', 'mainnet', 'bitcoin', 'testnet', 'signet'];
footerVisible = true;
user: any = undefined;
servicesEnabled = false;

View File

@@ -1,11 +1,10 @@
import { Component, ComponentRef, ViewChild, HostListener, Input, Output, EventEmitter,
import { Component, ViewChild, Input, Output, EventEmitter,
OnInit, OnDestroy, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef, AfterViewInit } from '@angular/core';
import { StateService } from '../../services/state.service';
import { MempoolBlockDelta } from '../../interfaces/websocket.interface';
import { MempoolBlockDelta, isMempoolDelta } from '../../interfaces/websocket.interface';
import { TransactionStripped } from '../../interfaces/node-api.interface';
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
import { Subscription, BehaviorSubject, merge, of, timer } from 'rxjs';
import { switchMap, filter, concatMap, map } from 'rxjs/operators';
import { Subscription, BehaviorSubject } from 'rxjs';
import { WebsocketService } from '../../services/websocket.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { Router } from '@angular/router';
@@ -39,10 +38,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
poolDirection: string = 'left';
blockSub: Subscription;
rateLimit = 1000;
private lastEventTime = Date.now() - this.rateLimit;
private subId = 0;
firstLoad: boolean = true;
constructor(
@@ -62,39 +57,13 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
}
ngAfterViewInit(): void {
this.blockSub = merge(
this.stateService.mempoolBlockTransactions$,
this.stateService.mempoolBlockDelta$,
).pipe(
concatMap(update => {
const now = Date.now();
const timeSinceLastEvent = now - this.lastEventTime;
this.lastEventTime = Math.max(now, this.lastEventTime + this.rateLimit);
const subId = this.subId;
// If time since last event is less than X seconds, delay this event
if (timeSinceLastEvent < this.rateLimit) {
return timer(this.rateLimit - timeSinceLastEvent).pipe(
// Emit the event after the timer
map(() => ({ update, subId }))
);
} else {
// If enough time has passed, emit the event immediately
return of({ update, subId });
}
})
).subscribe(({ update, subId }) => {
// discard stale updates after a block transition
if (subId !== this.subId) {
return;
}
this.blockSub = this.stateService.mempoolBlockUpdate$.subscribe((update) => {
// process update
if (update['added']) {
if (isMempoolDelta(update)) {
// delta
this.updateBlock(update as MempoolBlockDelta);
this.updateBlock(update);
} else {
const transactionsStripped = update as TransactionStripped[];
const transactionsStripped = update.transactions;
// new transactions
if (this.firstLoad) {
this.replaceBlock(transactionsStripped);
@@ -137,7 +106,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
ngOnChanges(changes): void {
if (changes.index) {
this.subId++;
this.firstLoad = true;
if (this.blockGraph) {
this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection);
@@ -173,7 +141,11 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
const direction = (this.blockIndex == null || this.index < this.blockIndex) ? this.poolDirection : this.chainDirection;
this.blockGraph.replace(delta.added, direction);
} else {
this.blockGraph.update(delta.added, delta.removed, delta.changed || [], blockMined ? this.chainDirection : this.poolDirection, blockMined);
if (blockMined) {
this.blockGraph.update(delta.added, delta.removed, delta.changed || [], blockMined ? this.chainDirection : this.poolDirection, blockMined);
} else {
this.blockGraph.deferredUpdate(delta.added, delta.removed, delta.changed || [], this.poolDirection);
}
}
this.lastBlockHeight = this.stateService.latestBlockHeight;

View File

@@ -56,7 +56,7 @@
.fee-span {
font-size: 11px;
margin-bottom: 5px;
color: #fff000;
color: var(--yellow);
}
.transaction-count {
@@ -119,7 +119,7 @@
height: 0;
border-left: calc(var(--block-size) * 0.3) solid transparent;
border-right: calc(var(--block-size) * 0.3) solid transparent;
border-bottom: calc(var(--block-size) * 0.3) solid #FFF;
border-bottom: calc(var(--block-size) * 0.3) solid var(--fg);
}
.blockLink {

View File

@@ -77,7 +77,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
}
this.isWidget = this.template === 'widget';
this.showCount = !this.isWidget && !this.hideCount;
this.windowPreference = this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference');
this.windowPreference = (this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference')) || '2h';
this.mempoolVsizeFeesData = this.handleNewMempoolData(this.data.concat([]));
this.mountFeeChart();
}
@@ -256,11 +256,17 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
const itemFormatted = [];
let sum = 0;
let progressPercentageText = '';
let countItem;
let items = this.inverted ? [...params].reverse() : params;
if (items[items.length - 1].seriesName === 'count') {
countItem = items.pop();
}
const unfilteredItems = this.inverted ? [...params].reverse() : params;
const countItem = unfilteredItems.find(p => p.seriesName === 'count');
const usedSeries = {};
const items = unfilteredItems.filter(p => {
if (usedSeries[p.seriesName] || p.seriesName === 'count') {
return false;
}
usedSeries[p.seriesName] = true;
return true;
});
items.map((item: any, index: number) => {
sum += item.value[1];
const progressPercentage = (item.value[1] / totalValue) * 100;

View File

@@ -63,7 +63,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -1,10 +1,10 @@
.rbf-tooltip {
position: fixed;
z-index: 3;
background: rgba(#11131f, 0.95);
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
color: #b1b1b1;
color: var(--tooltip-grey);
display: flex;
flex-direction: column;
justify-content: space-between;

View File

@@ -159,7 +159,7 @@
&.selected {
.shape-border {
background: #9339f4;
background: var(--mainnet-alt);
}
}
@@ -183,7 +183,7 @@
width: calc(1em + 16px);
.shape {
border: solid 4px #9339f4;
border: solid 4px var(--mainnet-alt);
}
&:hover {

View File

@@ -179,7 +179,7 @@ export class SearchFormComponent implements OnInit {
const lightningResults = result[1];
// Do not show date and timestamp results for liquid
const isNetworkBitcoin = this.network === '' || this.network === 'testnet' || this.network === 'signet';
const isNetworkBitcoin = this.network === '' || this.network === 'testnet' || this.network === 'testnet4' || this.network === 'signet';
const matchesBlockHeight = this.regexBlockheight.test(searchText) && parseInt(searchText) <= this.stateService.latestBlockHeight;
const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date' && new Date(searchText).getTime() <= Date.now() && isNetworkBitcoin;

View File

@@ -60,6 +60,9 @@
<ng-container *ngSwitchCase="'testnet'">
<ng-component *ngTemplateOutlet="bitcoinLogo; context: {$implicit: '#5fd15c', width, height, viewBox}"></ng-component>
</ng-container>
<ng-container *ngSwitchCase="'testnet4'">
<ng-component *ngTemplateOutlet="bitcoinLogo; context: {$implicit: '#5fd15c', width, height, viewBox}"></ng-component>
</ng-container>
<ng-container *ngSwitchCase="'liquid'">
<ng-component *ngTemplateOutlet="liquidLogo; context: {$implicit: '', width, height, viewBox, color1: '#2cccbf', color2: '#9ef2ed'}"></ng-component>
</ng-container>

View File

@@ -71,7 +71,9 @@ export class TelevisionComponent implements OnInit, OnDestroy {
mempoolStats = newStats;
} else if (['2h', '24h'].includes(this.fragment)) {
mempoolStats.unshift(newStats[0]);
mempoolStats = mempoolStats.slice(0, mempoolStats.length - 1);
const now = Date.now() / 1000;
const start = now - (this.fragment === '2h' ? (2 * 60 * 60) : (24 * 60 * 60) );
mempoolStats = mempoolStats.filter(p => p.added >= start);
}
return mempoolStats;
})

View File

@@ -11,7 +11,7 @@
<div class="text-left">
<p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from <a href="https://bgp.tools/as/142052#connectivity">AS142052</a>.</p>
<p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, the <a href="https://bitcoin.gob.sv/">bitcoin.gob.sv</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from <a href="https://bgp.tools/as/142052#connectivity">AS142052</a>.</p>
<p *ngIf="!officialMempoolSpace">This website and its API service (collectively, the "Website") are operated by a member of the Bitcoin community ("We" or "Us"). Mempool Space K.K. in Japan ("Mempool") has no affiliation with the operator of this Website, and does not sponsor or endorse the information provided herein.</p>

View File

@@ -0,0 +1,53 @@
<div class="container-xl">
<h1 class="text-left" i18n="shared.test-transactions|Test Transactions">Test Transactions</h1>
<form [formGroup]="testTxsForm" (submit)="testTxsForm.valid && testTxs()" novalidate>
<label for="maxfeerate" i18n="test.tx.raw-hex">Raw hex</label>
<div class="mb-3">
<textarea formControlName="txs" class="form-control" rows="5" i18n-placeholder="transaction.test-transactions" placeholder="Comma-separated list of raw transactions"></textarea>
</div>
<label for="maxfeerate" i18n="test.tx.max-fee-rate">Maximum fee rate (sat/vB)</label>
<input type="number" class="form-control input-dark" formControlName="maxfeerate" id="maxfeerate"
[value]="10000" placeholder="10,000 s/vb" [class]="{invalid: invalidMaxfeerate}">
<br>
<button [disabled]="isLoading" type="submit" class="btn btn-primary mr-2" i18n="shared.test-transactions|Test Transactions">Test Transactions</button>
<p class="red-color d-inline">{{ error }}</p>
</form>
<br>
<div class="box" *ngIf="results?.length">
<table class="accept-results table table-fixed table-borderless table-striped">
<tbody>
<tr>
<th class="allowed" i18n="test-tx.is-allowed">Allowed?</th>
<th class="txid" i18n="dashboard.latest-transactions.txid">TXID</th>
<th class="rate" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</th>
<th class="reason" i18n="test-tx.rejection-reason">Rejection reason</th>
</tr>
<ng-container *ngFor="let result of results;">
<tr>
<td class="allowed">
<ng-container [ngSwitch]="result.allowed">
<span *ngSwitchCase="true"></span>
<span *ngSwitchCase="false"></span>
<span *ngSwitchDefault>-</span>
</ng-container>
</td>
<td class="txid">
<app-truncate [text]="result.txid || '-'"></app-truncate>
</td>
<td class="rate">
<app-fee-rate *ngIf="result.fees?.['effective-feerate'] != null" [fee]="result.fees?.['effective-feerate'] * 100000"></app-fee-rate>
<span *ngIf="result.fees?.['effective-feerate'] == null">-</span>
</td>
<td class="reason">
{{ result['reject-reason'] || '-' }}
</td>
</tr>
</ng-container>
</tbody>
</table>
</div>
</div>

View File

@@ -0,0 +1,34 @@
.accept-results {
td, th {
&.allowed {
width: 10%;
text-align: center;
}
&.txid {
width: 50%;
}
&.rate {
width: 20%;
text-align: right;
white-space: wrap;
}
&.reason {
width: 20%;
text-align: right;
white-space: wrap;
}
}
@media (max-width: 950px) {
table-layout: auto;
td, th {
&.allowed {
width: 100px;
}
&.txid {
max-width: 200px;
}
}
}
}

View File

@@ -0,0 +1,86 @@
import { Component, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { ApiService } from '../../services/api.service';
import { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
import { OpenGraphService } from '../../services/opengraph.service';
import { TestMempoolAcceptResult } from '../../interfaces/node-api.interface';
@Component({
selector: 'app-test-transactions',
templateUrl: './test-transactions.component.html',
styleUrls: ['./test-transactions.component.scss']
})
export class TestTransactionsComponent implements OnInit {
testTxsForm: UntypedFormGroup;
error: string = '';
results: TestMempoolAcceptResult[] = [];
isLoading = false;
invalidMaxfeerate = false;
constructor(
private formBuilder: UntypedFormBuilder,
private apiService: ApiService,
public stateService: StateService,
private seoService: SeoService,
private ogService: OpenGraphService,
) { }
ngOnInit(): void {
this.testTxsForm = this.formBuilder.group({
txs: ['', Validators.required],
maxfeerate: ['', Validators.min(0)]
});
this.seoService.setTitle($localize`:@@meta.title.test-txs:Test Transactions`);
this.ogService.setManualOgImage('tx-push.jpg');
}
testTxs() {
let txs: string[] = [];
try {
txs = (this.testTxsForm.get('txs')?.value as string).split(',').map(hex => hex.trim());
if (!txs?.length) {
this.error = 'At least one transaction is required';
return;
} else if (txs.length > 25) {
this.error = 'Exceeded maximum of 25 transactions';
return;
}
} catch (e) {
this.error = e?.message;
return;
}
let maxfeerate;
this.invalidMaxfeerate = false;
try {
const maxfeerateVal = this.testTxsForm.get('maxfeerate')?.value;
if (maxfeerateVal != null && maxfeerateVal !== '') {
maxfeerate = parseFloat(maxfeerateVal) / 100_000;
}
} catch (e) {
this.invalidMaxfeerate = true;
}
this.isLoading = true;
this.error = '';
this.results = [];
this.apiService.testTransactions$(txs, maxfeerate === 0.1 ? null : maxfeerate)
.subscribe((result) => {
this.isLoading = false;
this.results = result || [];
this.testTxsForm.reset();
},
(error) => {
if (typeof error.error === 'string') {
const matchText = error.error.match('"message":"(.*?)"');
this.error = matchText && matchText[1] || error.error;
} else if (error.message) {
this.error = error.message;
}
this.isLoading = false;
});
}
}

View File

@@ -11,7 +11,7 @@ import { Subscription } from 'rxjs';
})
export class ThemeSelectorComponent implements OnInit {
themeForm: UntypedFormGroup;
themes = ['default', 'contrast', 'wiz'];
themes = ['default', 'contrast', 'wiz', 'bukele'];
themeSubscription: Subscription;
constructor(

View File

@@ -1,15 +1,34 @@
<div class="mobile-wrapper">
<div class="mobile-container">
<div class="panel">
<div class="field nav-header">
<app-svg-images name="officialMempoolSpace" style="width: 144px; height: 36px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
<div [ngSwitch]="network" class="network-label">
<div class="nav-header">
@if (enterpriseInfo?.header_img) {
<a class="d-flex" [routerLink]="['/' | relativeUrl]">
<img *ngIf="enterpriseInfo.header_img" [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="42px">
</a>
} @else if (enterpriseInfo?.img || enterpriseInfo?.imageMd5) {
<a [routerLink]="['/' | relativeUrl]">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + enterpriseInfo.name + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
</a>
<div class="vertical-line"></div>
}
@if (!enterpriseInfo?.header_img) {
<a [routerLink]="['/' | relativeUrl]">
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" style="width: 144px; height: 36px" viewBox="0 0 500 126" width="500" height="126" class="mempool-logo"></app-svg-images>
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 144px; height: 36px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
</a>
}
@if (enterpriseInfo?.header_img || (!enterpriseInfo?.img && !enterpriseInfo?.imageMd5)) {
<div [ngSwitch]="network" class="network-label" [class.hide-name]="enterpriseInfo?.header_img">
<span *ngSwitchCase="'signet'" class="network signet"><span class="name">Bitcoin Signet</span><app-svg-images name="signet" width="35" height="35" viewBox="0 0 65 65" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span>
<span *ngSwitchCase="'testnet'" class="network testnet"><span class="name">Bitcoin Testnet</span><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span>
<span *ngSwitchCase="'testnet'" class="network testnet"><span class="name">Bitcoin Testnet3</span><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span>
<span *ngSwitchCase="'testnet4'" class="network testnet"><span class="name">Bitcoin Testnet4</span><app-svg-images name="testnet4" width="35" height="35" viewBox="0 0 65 65" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span>
<span *ngSwitchCase="'liquid'" class="network liquid"><span class="name">Liquid</span><app-svg-images name="liquid" width="35" height="35" viewBox="0 0 125 125" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span>
<span *ngSwitchCase="'liquidtestnet'" class="network liquidtestnet"><span class="name">Liquid Testnet</span><app-svg-images name="liquidtestnet" width="35" height="35" viewBox="0 0 125 125" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span>
<span *ngSwitchDefault class="network mainnet"><span class="name">Bitcoin</span><app-svg-images name="bitcoin" width="35" height="35" viewBox="0 0 65 65" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span>
</div>
}
</div>
<div class="field">
<div class="label" i18n="shared.transaction">Transaction</div>

View File

@@ -40,7 +40,14 @@
}
.nav-header {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
width: 100%;
max-width: 100%;
padding: 1em;
position: relative;
background: var(--nav-bg);
box-shadow: 0 -5px 15px #000;
z-index: 100;
align-items: center;
@@ -53,6 +60,40 @@
flex-direction: row;
align-items: center;
}
&.hide-name .name {
display: none;
}
}
.subdomain_logo {
height: 35px;
overflow: clip;
max-width: 140px;
margin: auto;
align-self: center;
.rounded {
border-radius: 5px;
}
}
.subdomain_container {
max-width: 140px;
text-align: center;
align-self: center;
}
.vertical-line {
border-left: 1px solid #444;
height: 30px;
margin-left: 10px;
margin-right: 10px;
margin-top: 3px;
}
.logo-holder {
display: flex;
flex-direction: row;
}
}

View File

@@ -113,6 +113,10 @@ export class TrackerComponent implements OnInit, OnDestroy {
scrollIntoAccelPreview = false;
auditEnabled: boolean = this.stateService.env.AUDIT && this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true;
enterpriseInfo: any;
enterpriseInfo$: Subscription;
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
constructor(
private route: ActivatedRoute,
private electrsApiService: ElectrsApiService,
@@ -152,6 +156,10 @@ export class TrackerComponent implements OnInit, OnDestroy {
this.enterpriseService.page();
this.enterpriseInfo$ = this.enterpriseService.info$.subscribe(info => {
this.enterpriseInfo = info;
});
this.websocketService.want(['blocks', 'mempool-blocks']);
this.stateService.networkChanged$.subscribe(
(network) => {
@@ -702,6 +710,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
this.blocksSubscription.unsubscribe();
this.miningSubscription?.unsubscribe();
this.currencyChangeSubscription?.unsubscribe();
this.enterpriseInfo$?.unsubscribe();
this.leaveTransaction();
}
}

View File

@@ -103,7 +103,8 @@ td.amount.large {
margin-top: 10px;
}
.assetBox {
background-color: #653b9c90;
background: color-mix(in srgb, var(--tertiary) 56%, transparent);
}
.details-container {

View File

@@ -0,0 +1,16 @@
@if (loading) {
<div class="spinner-wrapper">
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
} @else if (error) {
<div class="error-wrapper">
<span>failed to load X timeline</span>
</div>
}
<iframe id="twitter-widget-0" scrolling="no" frameborder="0" allowtransparency="true" allowfullscreen="true"
title="Twitter Timeline"
[src]="iframeSrc"
style="position: static; visibility: visible; width: 100%; height: 100%; display: block; flex-grow: 1;"
(load)="onReady()"
></iframe>

View File

@@ -0,0 +1,10 @@
.spinner-wrapper, .error-wrapper {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
}

View File

@@ -0,0 +1,71 @@
import { Component, Input, ChangeDetectionStrategy, SecurityContext, SimpleChanges, OnChanges } from '@angular/core';
import { LanguageService } from '../../services/language.service';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-twitter-widget',
templateUrl: './twitter-widget.component.html',
styleUrls: ['./twitter-widget.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TwitterWidgetComponent implements OnChanges {
@Input() handle: string;
@Input() width = 300;
@Input() height = 400;
loading: boolean = true;
error: boolean = false;
lang: string = 'en';
iframeSrc: SafeResourceUrl;
constructor(
private languageService: LanguageService,
public sanitizer: DomSanitizer,
) {
this.lang = this.languageService.getLanguage();
this.setIframeSrc();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.handle) {
this.setIframeSrc();
}
}
setIframeSrc(): void {
if (this.handle) {
this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL,
`https://syndication.twitter.com/srv/timeline-profile/screen-name/${this.handle}?creatorScreenName=mempool`
+ '&dnt=true'
+ '&embedId=twitter-widget-0'
+ '&features=eyJ0ZndfdGltZWxpbmVfgbGlzdCI6eyJidWNrZXQiOltdLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X2ZvbGxvd2VyX2NvdW50X3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9iYWNrZW5kIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19yZWZzcmNfc2Vzc2lvbiI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfZm9zbnJfc29mdF9pbnRlcnZlbnRpb25zX2VuYWJsZWQiOnsiYnVja2V0Ijoib24iLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X21peGVkX21lZGlhXzE1ODk3Ijp7ImJ1Y2tldCI6InRyZWF0bWVudCIsInZlcnNpb24iOm51bGx9LCJ0ZndfZXhwZXJpbWVudHNfY29va2llX2V4cGlyYXRpb24iOnsiYnVja2V0IjoxMjA5NjAwLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X3Nob3dfYmlyZHdhdGNoX3Bpdm90c19lbmFibGVkIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19kdXBsaWNhdGVfc2NyaWJlc190b19zZXR0aW5ncyI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdXNlX3Byb2ZpbGVfaW1hZ2Vfc2hhcGVfZW5hYmxlZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdmlkZW9faGxzX2R5bmFtaWNfbWFuaWZlc3RzXzE1MDgyIjp7ImJ1Y2tldCI6InRydWVfYml0cmF0ZSIsInZlcnNpb24iOm51bGx9LCJ0ZndfbGVnYWN5X3RpbWVsaW5lX3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9mcm9udGVuZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9fQ%3D%3D'
+ '&frame=false'
+ '&hideBorder=true'
+ '&hideFooter=false'
+ '&hideHeader=true'
+ '&hideScrollBar=false'
+ `&lang=${this.lang}`
+ '&maxHeight=500px'
+ '&origin=https%3A%2F%2Fmempool.space%2F'
// + '&sessionId=88f6d661d0dcca99c43c0a590f6a3e61c89226a9'
+ '&showHeader=false'
+ '&showReplies=false'
+ '&siteScreenName=mempool'
+ '&theme=dark'
+ '&transparent=true'
+ '&widgetsVersion=2615f7e52b7e0%3A1702314776716'
));
}
}
onReady(): void {
this.loading = false;
this.error = false;
}
onFailed(): void {
this.loading = false;
this.error = true;
}
}

View File

@@ -1,6 +1,6 @@
.bowtie-graph-tooltip {
position: absolute;
background: rgba(#11131f, 0.95);
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
color: var(--tooltip-grey);

View File

@@ -84,18 +84,19 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
refreshOutspends$: ReplaySubject<string> = new ReplaySubject();
gradientColors = {
'': ['#9339f4', '#105fb0', '#9339f400'],
'': ['var(--mainnet-alt)', 'var(--primary)', 'color-mix(in srgb, var(--mainnet-alt) 1%, transparent)'],
// liquid: ['#116761', '#183550'],
liquid: ['#09a197', '#0f62af', '#09a19700'],
// 'liquidtestnet': ['#494a4a', '#272e46'],
'liquidtestnet': ['#d2d2d2', '#979797', '#d2d2d200'],
// testnet: ['#1d486f', '#183550'],
testnet: ['#4edf77', '#10a0af', '#4edf7700'],
testnet4: ['#4edf77', '#10a0af', '#4edf7700'],
// signet: ['#6f1d5d', '#471850'],
signet: ['#d24fc8', '#a84fd2', '#d24fc800'],
};
gradient: string[] = ['#105fb0', '#105fb0'];
gradient: string[] = ['var(--primary)', 'var(--primary)'];
constructor(
private router: Router,