Merge branch 'master' into natsoni/add-block-fee-graph
This commit is contained in:
@@ -53,6 +53,44 @@ let routes: Routes = [
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'testnet4',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: StatusViewComponent
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: '/testnet4'
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'signet',
|
||||
children: [
|
||||
@@ -130,6 +168,10 @@ let routes: Routes = [
|
||||
path: 'testnet',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
{
|
||||
path: 'testnet4',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
{
|
||||
path: 'signet',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
|
||||
@@ -189,22 +189,22 @@ export const specialBlocks = {
|
||||
'0': {
|
||||
labelEvent: 'Genesis',
|
||||
labelEventCompleted: 'The Genesis of Bitcoin',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'210000': {
|
||||
labelEvent: 'Bitcoin\'s 1st Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 25 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'420000': {
|
||||
labelEvent: 'Bitcoin\'s 2nd Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 12.5 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'630000': {
|
||||
labelEvent: 'Bitcoin\'s 3rd Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 6.25 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'709632': {
|
||||
labelEvent: 'Taproot 🌱 activation',
|
||||
@@ -214,62 +214,62 @@ export const specialBlocks = {
|
||||
'840000': {
|
||||
labelEvent: 'Bitcoin\'s 4th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'1050000': {
|
||||
labelEvent: 'Bitcoin\'s 5th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 1.5625 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'1260000': {
|
||||
labelEvent: 'Bitcoin\'s 6th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.78125 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'1470000': {
|
||||
labelEvent: 'Bitcoin\'s 7th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.390625 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'1680000': {
|
||||
labelEvent: 'Bitcoin\'s 8th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.1953125 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'1890000': {
|
||||
labelEvent: 'Bitcoin\'s 9th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.09765625 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'2100000': {
|
||||
labelEvent: 'Bitcoin\'s 10th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.04882812 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'2310000': {
|
||||
labelEvent: 'Bitcoin\'s 11th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.02441406 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'2520000': {
|
||||
labelEvent: 'Bitcoin\'s 12th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.01220703 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'2730000': {
|
||||
labelEvent: 'Bitcoin\'s 13th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.00610351 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'2940000': {
|
||||
labelEvent: 'Bitcoin\'s 14th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.00305175 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'3150000': {
|
||||
labelEvent: 'Bitcoin\'s 15th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.00152587 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -266,6 +266,11 @@ const featureActivation = {
|
||||
segwit: 872730,
|
||||
taproot: 2032291,
|
||||
},
|
||||
testnet4: {
|
||||
rbf: 0,
|
||||
segwit: 0,
|
||||
taproot: 0,
|
||||
},
|
||||
signet: {
|
||||
rbf: 0,
|
||||
segwit: 0,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)">
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
.qr-wrapper {
|
||||
background-color: var(--fg);
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
padding-bottom: 5px;
|
||||
display: inline-block;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.qr-wrapper {
|
||||
background-color: var(--fg);
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
padding-bottom: 5px;
|
||||
display: inline-block;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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%
|
||||
)`,
|
||||
|
||||
@@ -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> </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> </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>
|
||||
|
||||
@@ -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();
|
||||
@@ -283,8 +285,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 +301,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 +371,6 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
|
||||
this.goggleResolution = 86;
|
||||
this.graphHeight = 310;
|
||||
}
|
||||
this.isMobile = window.innerWidth <= 767.98;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Component, Input, ChangeDetectionStrategy, SecurityContext } 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 {
|
||||
@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();
|
||||
}
|
||||
|
||||
setIframeSrc(): void {
|
||||
this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL,
|
||||
'https://syndication.twitter.com/srv/timeline-profile/screen-name/bitcoinofficesv?creatorScreenName=mempool'
|
||||
+ '&dnt=true'
|
||||
+ '&embedId=twitter-widget-0'
|
||||
+ '&features=eyJ0ZndfdGltZWxpbmVfbGlzdCI6eyJidWNrZXQiOltdLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X2ZvbGxvd2VyX2NvdW50X3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9iYWNrZW5kIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19yZWZzcmNfc2Vzc2lvbiI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfZm9zbnJfc29mdF9pbnRlcnZlbnRpb25zX2VuYWJsZWQiOnsiYnVja2V0Ijoib24iLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X21peGVkX21lZGlhXzE1ODk3Ijp7ImJ1Y2tldCI6InRyZWF0bWVudCIsInZlcnNpb24iOm51bGx9LCJ0ZndfZXhwZXJpbWVudHNfY29va2llX2V4cGlyYXRpb24iOnsiYnVja2V0IjoxMjA5NjAwLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X3Nob3dfYmlyZHdhdGNoX3Bpdm90c19lbmFibGVkIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19kdXBsaWNhdGVfc2NyaWJlc190b19zZXR0aW5ncyI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdXNlX3Byb2ZpbGVfaW1hZ2Vfc2hhcGVfZW5hYmxlZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdmlkZW9faGxzX2R5bmFtaWNfbWFuaWZlc3RzXzE1MDgyIjp7ImJ1Y2tldCI6InRydWVfYml0cmF0ZSIsInZlcnNpb24iOm51bGx9LCJ0ZndfbGVnYWN5X3RpbWVsaW5lX3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9mcm9udGVuZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9fQ%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 {
|
||||
console.log('ready!');
|
||||
this.loading = false;
|
||||
this.error = false;
|
||||
}
|
||||
|
||||
onFailed(): void {
|
||||
console.log('failed!')
|
||||
this.loading = false;
|
||||
this.error = true;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -301,7 +301,8 @@
|
||||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
color: var(--fg);
|
||||
opacity: var(--opacity);
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
@@ -435,7 +436,8 @@
|
||||
|
||||
.in-progress-message {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
color: var(--fg);
|
||||
opacity: var(--opacity);
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const bitcoinNetworks = ["", "testnet", "signet"];
|
||||
const bitcoinNetworks = ["", "testnet", "testnet4", "signet"];
|
||||
const liquidNetworks = ["liquid", "liquidtestnet"];
|
||||
const lightningNetworks = ["", "testnet", "signet"];
|
||||
const miningTimeIntervals = "<code>24h</code>, <code>3d</code>, <code>1w</code>, <code>1m</code>, <code>3m</code>, <code>6m</code>, <code>1y</code>, <code>2y</code>, <code>3y</code>";
|
||||
|
||||
const emptyCodeSample = {
|
||||
@@ -6513,7 +6514,7 @@ export const restApiDocsData = [
|
||||
category: "lightning",
|
||||
fragment: "lightning",
|
||||
title: "Lightning",
|
||||
showConditions: bitcoinNetworks
|
||||
showConditions: lightningNetworks
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
@@ -6525,7 +6526,7 @@ export const restApiDocsData = [
|
||||
default: "<p>Returns network-wide stats such as total number of channels and nodes, total capacity, and average/median fee figures.</p><p>Pass one of the following for <code>:interval</code>: <code>latest</code>, <code>24h</code>, <code>3d</code>, <code>1w</code>, <code>1m</code>, <code>3m</code>, <code>6m</code>, <code>1y</code>, <code>2y</code>, <code>3y</code>.</p>"
|
||||
},
|
||||
urlString: "/v1/lightning/statistics/:interval",
|
||||
showConditions: bitcoinNetworks,
|
||||
showConditions: lightningNetworks,
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
@@ -6621,7 +6622,7 @@ export const restApiDocsData = [
|
||||
default: "<p>Returns Lightning nodes and channels that match a full-text, case-insensitive search <code>:query</code> across node aliases, node pubkeys, channel IDs, and short channel IDs.</p>"
|
||||
},
|
||||
urlString: "/v1/lightning/search?searchText=:query",
|
||||
showConditions: bitcoinNetworks,
|
||||
showConditions: lightningNetworks,
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
@@ -6706,7 +6707,7 @@ export const restApiDocsData = [
|
||||
default: "<p>Returns a list of Lightning nodes running on clearnet in the requested <code>:country</code>, where <code>:country</code> is an ISO Alpha-2 country code.</p>"
|
||||
},
|
||||
urlString: "/v1/lightning/nodes/country/:country",
|
||||
showConditions: bitcoinNetworks,
|
||||
showConditions: lightningNetworks,
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
@@ -6928,7 +6929,7 @@ export const restApiDocsData = [
|
||||
default: "<p>Returns aggregate capacity and number of clearnet nodes per country. Capacity figures are in satoshis.</p>"
|
||||
},
|
||||
urlString: "/v1/lightning/nodes/countries",
|
||||
showConditions: bitcoinNetworks,
|
||||
showConditions: lightningNetworks,
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
@@ -7072,7 +7073,7 @@ export const restApiDocsData = [
|
||||
default: "<p>Returns a list of nodes hosted by a specified <code>:isp</code>, where <code>:isp</code> is an ISP's ASN.</p>"
|
||||
},
|
||||
urlString: "/v1/lightning/nodes/isp/:isp",
|
||||
showConditions: bitcoinNetworks,
|
||||
showConditions: lightningNetworks,
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
@@ -7191,7 +7192,7 @@ export const restApiDocsData = [
|
||||
default: "<p>Returns aggregate capacity, number of nodes, and number of channels per ISP. Capacity figures are in satoshis.</p>"
|
||||
},
|
||||
urlString: "/v1/lightning/nodes/isp-ranking",
|
||||
showConditions: bitcoinNetworks,
|
||||
showConditions: lightningNetworks,
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
@@ -7303,7 +7304,7 @@ export const restApiDocsData = [
|
||||
default: "<p>Returns two lists of the top 100 nodes: one ordered by liquidity (aggregate channel capacity) and the other ordered by connectivity (number of open channels).</p>"
|
||||
},
|
||||
urlString: "/v1/lightning/nodes/rankings",
|
||||
showConditions: bitcoinNetworks,
|
||||
showConditions: lightningNetworks,
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
@@ -7426,7 +7427,7 @@ export const restApiDocsData = [
|
||||
default: "<p>Returns a list of the top 100 nodes by liquidity (aggregate channel capacity).</p>"
|
||||
},
|
||||
urlString: "/v1/lightning/nodes/rankings/liquidity",
|
||||
showConditions: bitcoinNetworks,
|
||||
showConditions: lightningNetworks,
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
@@ -7623,7 +7624,7 @@ export const restApiDocsData = [
|
||||
default: "<p>Returns a list of the top 100 nodes by connectivity (number of open channels).</p>"
|
||||
},
|
||||
urlString: "/v1/lightning/nodes/rankings/connectivity",
|
||||
showConditions: bitcoinNetworks,
|
||||
showConditions: lightningNetworks,
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
@@ -7819,7 +7820,7 @@ export const restApiDocsData = [
|
||||
default: "<p>Returns a list of the top 100 oldest nodes.</p>"
|
||||
},
|
||||
urlString: "/v1/lightning/nodes/rankings/age",
|
||||
showConditions: bitcoinNetworks,
|
||||
showConditions: lightningNetworks,
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
@@ -8006,7 +8007,7 @@ export const restApiDocsData = [
|
||||
default: "<p>Returns details about a node with the given <code>:pubKey</code>.</p>"
|
||||
},
|
||||
urlString: "/v1/lightning/nodes/:pubKey",
|
||||
showConditions: bitcoinNetworks,
|
||||
showConditions: lightningNetworks,
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
@@ -8170,7 +8171,7 @@ export const restApiDocsData = [
|
||||
default: "<p>Returns historical stats for a node with the given <code>:pubKey</code>.</p>"
|
||||
},
|
||||
urlString: "/v1/lightning/nodes/:pubKey/statistics",
|
||||
showConditions: bitcoinNetworks,
|
||||
showConditions: lightningNetworks,
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
@@ -8268,7 +8269,7 @@ export const restApiDocsData = [
|
||||
default: "<p>Returns info about a Lightning channel with the given <code>:channelId</code>.</p>"
|
||||
},
|
||||
urlString: "/v1/lightning/channels/:channelId",
|
||||
showConditions: bitcoinNetworks,
|
||||
showConditions: lightningNetworks,
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
@@ -8433,7 +8434,7 @@ export const restApiDocsData = [
|
||||
default: "<p>Returns channels that correspond to the given <code>:txid</code> (multiple transaction IDs can be specified).</p>"
|
||||
},
|
||||
urlString: "/v1/lightning/channels/txids?txId[]=:txid",
|
||||
showConditions: bitcoinNetworks,
|
||||
showConditions: lightningNetworks,
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
@@ -8634,7 +8635,7 @@ export const restApiDocsData = [
|
||||
default: "<p>Returns a list of a node's channels given its <code>:pubKey</code>. Ten channels are returned at a time. Use <code>:index</code> for paging. <code>:channelStatus</code> can be <code>open</code>, <code>active</code>, or <code>closed</code>.</p>"
|
||||
},
|
||||
urlString: "/v1/lightning/channels?public_key=:pubKey&status=:channelStatus",
|
||||
showConditions: bitcoinNetworks,
|
||||
showConditions: lightningNetworks,
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
@@ -8770,7 +8771,7 @@ export const restApiDocsData = [
|
||||
default: "<p>Returns a list of channels with corresponding node geodata.</p>"
|
||||
},
|
||||
urlString: "/v1/lightning/channels-geo",
|
||||
showConditions: bitcoinNetworks,
|
||||
showConditions: lightningNetworks,
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
@@ -8878,7 +8879,7 @@ export const restApiDocsData = [
|
||||
default: "<p>Returns a list of channels with corresponding geodata for a node with the given <code>:pubKey</code>.</p>"
|
||||
},
|
||||
urlString: "/v1/lightning/channels-geo/:pubKey",
|
||||
showConditions: bitcoinNetworks,
|
||||
showConditions: lightningNetworks,
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
<p>{{electrsPort}}</p>
|
||||
<p class="subtitle">SSL</p>
|
||||
<p>Enabled</p>
|
||||
<p class="note" *ngIf="network.val !== 'signet'">Electrum RPC interface for Bitcoin Signet is <a href="/signet/docs/api/electrs">publicly available</a>. Electrum RPC interface for all other networks is available to <a href='https://mempool.space/enterprise'>sponsors</a> only—whitelisting is required.</p>
|
||||
<p class="note" *ngIf="network.val !== 'signet' && network.val !== 'testnet4'">Electrum RPC interface for <a href="/signet/docs/api/electrs">Bitcoin Signet</a> and <a href="/testnet4/docs/api/electrs">Bitcoin Testnet4</a> is publicly available. Electrum RPC interface for all other networks is available to <a href='https://mempool.space/enterprise'>sponsors</a> only—whitelisting is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -102,6 +102,8 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
|
||||
this.electrsPort = 50002; break;
|
||||
case "testnet":
|
||||
this.electrsPort = 60002; break;
|
||||
case "testnet4":
|
||||
this.electrsPort = 40002; break;
|
||||
case "signet":
|
||||
this.electrsPort = 60602; break;
|
||||
case "liquid":
|
||||
@@ -170,6 +172,9 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
|
||||
if (network === 'testnet') {
|
||||
curlResponse = code.codeSampleTestnet.curl;
|
||||
}
|
||||
if (network === 'testnet4') {
|
||||
curlResponse = code.codeSampleTestnet.curl;
|
||||
}
|
||||
if (network === 'signet') {
|
||||
curlResponse = code.codeSampleSignet.curl;
|
||||
}
|
||||
|
||||
@@ -111,7 +111,10 @@ export class CodeTemplateComponent implements OnInit {
|
||||
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleMainnet.esModule);
|
||||
}
|
||||
if (this.network === 'testnet') {
|
||||
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleTestnet.esModule);
|
||||
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleTestnet.esModule);
|
||||
}
|
||||
if (this.network === 'testnet4') {
|
||||
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleTestnet.esModule);
|
||||
}
|
||||
if (this.network === 'signet') {
|
||||
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleSignet.esModule);
|
||||
@@ -144,7 +147,10 @@ init();`;
|
||||
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleMainnet.esModule);
|
||||
}
|
||||
if (this.network === 'testnet') {
|
||||
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleTestnet.esModule);
|
||||
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleTestnet.esModule);
|
||||
}
|
||||
if (this.network === 'testnet4') {
|
||||
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleTestnet.esModule);
|
||||
}
|
||||
if (this.network === 'signet') {
|
||||
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleSignet.esModule);
|
||||
@@ -212,6 +218,9 @@ yarn add @mempool/liquid.js`;
|
||||
if (this.network === 'testnet') {
|
||||
return this.replaceCurlPlaceholder(code.codeTemplate.curl, code.codeSampleTestnet);
|
||||
}
|
||||
if (this.network === 'testnet4') {
|
||||
return this.replaceCurlPlaceholder(code.codeTemplate.curl, code.codeSampleTestnet);
|
||||
}
|
||||
if (this.network === 'signet') {
|
||||
return this.replaceCurlPlaceholder(code.codeTemplate.curl, code.codeSampleSignet);
|
||||
}
|
||||
@@ -234,6 +243,9 @@ yarn add @mempool/liquid.js`;
|
||||
if (this.network === 'testnet') {
|
||||
return code.codeSampleTestnet.response;
|
||||
}
|
||||
if (this.network === 'testnet4') {
|
||||
return code.codeSampleTestnet.response;
|
||||
}
|
||||
if (this.network === 'signet') {
|
||||
return code.codeSampleSignet.response;
|
||||
}
|
||||
@@ -247,7 +259,7 @@ yarn add @mempool/liquid.js`;
|
||||
}
|
||||
|
||||
wrapPythonTemplate(code: any) {
|
||||
return ( ( this.network === 'testnet' || this.network === 'signet' ) ? ( code.codeTemplate.python.replace( "wss://mempool.space/api/v1/ws", "wss://mempool.space/" + this.network + "/api/v1/ws" ) ) : code.codeTemplate.python );
|
||||
return ( ( this.network === 'testnet' || this.network === 'testnet4' || this.network === 'signet' ) ? ( code.codeTemplate.python.replace( "wss://mempool.space/api/v1/ws", "wss://mempool.space/" + this.network + "/api/v1/ws" ) ) : code.codeTemplate.python );
|
||||
}
|
||||
|
||||
replaceJSPlaceholder(text: string, code: any) {
|
||||
|
||||
@@ -423,4 +423,17 @@ export interface AccelerationInfo {
|
||||
effective_fee: number,
|
||||
boost_rate: number,
|
||||
boost_cost: number,
|
||||
}
|
||||
|
||||
export interface TestMempoolAcceptResult {
|
||||
txid: string,
|
||||
wtxid: string,
|
||||
allowed?: boolean,
|
||||
vsize?: number,
|
||||
fees?: {
|
||||
base: number,
|
||||
"effective-feerate": number,
|
||||
"effective-includes": string[],
|
||||
},
|
||||
['reject-reason']?: string,
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SafeResourceUrl } from '@angular/platform-browser';
|
||||
import { ILoadingIndicators } from '../services/state.service';
|
||||
import { Transaction } from './electrs.interface';
|
||||
import { BlockExtended, DifficultyAdjustment, RbfTree, TransactionStripped } from './node-api.interface';
|
||||
import { Acceleration, BlockExtended, DifficultyAdjustment, RbfTree, TransactionStripped } from './node-api.interface';
|
||||
|
||||
export interface WebsocketResponse {
|
||||
backend?: 'esplora' | 'electrum' | 'none';
|
||||
@@ -35,6 +35,7 @@ export interface WebsocketResponse {
|
||||
'track-mempool-block'?: number;
|
||||
'track-rbf'?: string;
|
||||
'track-rbf-summary'?: boolean;
|
||||
'track-accelerations'?: boolean;
|
||||
'watch-mempool'?: boolean;
|
||||
'refresh-blocks'?: boolean;
|
||||
}
|
||||
@@ -75,6 +76,16 @@ export interface MempoolBlockDelta {
|
||||
removed: string[];
|
||||
changed: { txid: string, rate: number, flags: number, acc: boolean }[];
|
||||
}
|
||||
export interface MempoolBlockState {
|
||||
transactions: TransactionStripped[];
|
||||
}
|
||||
export type MempoolBlockUpdate = MempoolBlockDelta | MempoolBlockState;
|
||||
export function isMempoolState(update: MempoolBlockUpdate): update is MempoolBlockState {
|
||||
return update['transactions'] !== undefined;
|
||||
}
|
||||
export function isMempoolDelta(update: MempoolBlockUpdate): update is MempoolBlockDelta {
|
||||
return update['transactions'] === undefined;
|
||||
}
|
||||
|
||||
export interface MempoolBlockDeltaCompressed {
|
||||
added: TransactionCompressed[];
|
||||
@@ -82,6 +93,12 @@ export interface MempoolBlockDeltaCompressed {
|
||||
changed: MempoolDeltaChange[];
|
||||
}
|
||||
|
||||
export interface AccelerationDelta {
|
||||
added: Acceleration[];
|
||||
removed: string[];
|
||||
reset?: boolean;
|
||||
}
|
||||
|
||||
export interface MempoolInfo {
|
||||
loaded: boolean; // (boolean) True if the mempool is fully loaded
|
||||
size: number; // (numeric) Current tx count
|
||||
|
||||
@@ -66,7 +66,8 @@
|
||||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
color: var(--fg);
|
||||
opacity: var(--opacity);
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SharedModule } from './shared/shared.module';
|
||||
|
||||
import { StartComponent } from './components/start/start.component';
|
||||
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
|
||||
import { TestTransactionsComponent } from './components/test-transactions/test-transactions.component';
|
||||
import { CalculatorComponent } from './components/calculator/calculator.component';
|
||||
import { BlocksList } from './components/blocks-list/blocks-list.component';
|
||||
import { RbfList } from './components/rbf-list/rbf-list.component';
|
||||
@@ -30,6 +31,10 @@ const routes: Routes = [
|
||||
path: 'tx/push',
|
||||
component: PushTransactionComponent,
|
||||
},
|
||||
{
|
||||
path: 'tx/test',
|
||||
component: TestTransactionsComponent,
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
loadChildren: () => import('./components/about/about.module').then(m => m.AboutModule),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
|
||||
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
|
||||
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit, Acceleration, AccelerationHistoryParams, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo, RecentPeg, PegsVolume, AccelerationInfo } from '../interfaces/node-api.interface';
|
||||
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';
|
||||
import { BehaviorSubject, Observable, catchError, filter, map, of, shareReplay, take, tap } from 'rxjs';
|
||||
import { StateService } from './state.service';
|
||||
import { Transaction } from '../interfaces/electrs.interface';
|
||||
@@ -238,6 +238,10 @@ export class ApiService {
|
||||
return this.httpClient.post<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'});
|
||||
}
|
||||
|
||||
testTransactions$(rawTxs: string[], maxfeerate?: number): Observable<TestMempoolAcceptResult[]> {
|
||||
return this.httpClient.post<TestMempoolAcceptResult[]>(this.apiBaseUrl + this.apiBasePath + `/api/txs/test${maxfeerate != null ? '?maxfeerate=' + maxfeerate.toFixed(8) : ''}`, rawTxs);
|
||||
}
|
||||
|
||||
getTransactionStatus$(txid: string): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + txid + '/status');
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ export class EnterpriseService {
|
||||
|
||||
disableSubnetworks(): void {
|
||||
this.stateService.env.TESTNET_ENABLED = false;
|
||||
this.stateService.env.TESTNET4_ENABLED = false;
|
||||
this.stateService.env.LIQUID_ENABLED = false;
|
||||
this.stateService.env.LIQUID_TESTNET_ENABLED = false;
|
||||
this.stateService.env.SIGNET_ENABLED = false;
|
||||
|
||||
@@ -58,7 +58,7 @@ export class MiningService {
|
||||
// I think it's fine to hardcode this since we don't have x1000 hashrate jump everyday
|
||||
// If we want to support the mining dashboard for testnet, we can hardcode it too
|
||||
let selectedPower = 18;
|
||||
if (this.stateService.network === 'testnet') {
|
||||
if (this.stateService.network === 'testnet' || this.stateService.network === 'testnet4') {
|
||||
selectedPower = 12;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ const networkModules = {
|
||||
subnets: [
|
||||
{ name: 'mainnet', path: '' },
|
||||
{ name: 'testnet', path: '/testnet' },
|
||||
{ name: 'testnet4', path: '/testnet4' },
|
||||
{ name: 'signet', path: '/signet' },
|
||||
],
|
||||
},
|
||||
@@ -68,7 +69,7 @@ export class NavigationService {
|
||||
}
|
||||
if (route.url?.length) {
|
||||
path = [path, ...route.url.map(segment => segment.path).filter(path => {
|
||||
return path.length && !['testnet', 'signet'].includes(path);
|
||||
return path.length && !['testnet', 'testnet4', 'signet'].includes(path);
|
||||
})].join('/');
|
||||
}
|
||||
route = route.firstChild;
|
||||
|
||||
@@ -81,7 +81,9 @@ export class SeoService {
|
||||
|
||||
getTitle(): string {
|
||||
if (this.network === 'testnet')
|
||||
return this.baseTitle + ' - Bitcoin Testnet';
|
||||
return this.baseTitle + ' - Bitcoin Testnet3';
|
||||
if (this.network === 'testnet4')
|
||||
return this.baseTitle + ' - Bitcoin Testnet4';
|
||||
if (this.network === 'signet')
|
||||
return this.baseTitle + ' - Bitcoin Signet';
|
||||
if (this.network === 'liquid')
|
||||
@@ -92,7 +94,7 @@ export class SeoService {
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
if ( (this.network === 'testnet') || (this.network === 'signet') || (this.network === '') || (this.network == 'mainnet') )
|
||||
if ( (this.network === 'testnet') || (this.network === 'testnet4') || (this.network === 'signet') || (this.network === '') || (this.network == 'mainnet') )
|
||||
return this.baseDescription + ' See the real-time status of your transactions, browse network stats, and more.';
|
||||
if ( (this.network === 'liquid') || (this.network === 'liquidtestnet') )
|
||||
return this.baseDescription + ' See Liquid transactions & assets, get network info, and more.';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
|
||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
|
||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
|
||||
import { Transaction } from '../interfaces/electrs.interface';
|
||||
import { HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo } from '../interfaces/websocket.interface';
|
||||
import { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface';
|
||||
import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '../interfaces/websocket.interface';
|
||||
import { Acceleration, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface';
|
||||
import { Router, NavigationStart } from '@angular/router';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { filter, map, scan, shareReplay } from 'rxjs/operators';
|
||||
@@ -27,12 +27,15 @@ export interface Customization {
|
||||
name: string;
|
||||
site_id?: number;
|
||||
title: string;
|
||||
img: string;
|
||||
img?: string;
|
||||
header_img?: string;
|
||||
footer_img?: string;
|
||||
rounded_corner: boolean;
|
||||
},
|
||||
dashboard: {
|
||||
widgets: {
|
||||
component: string;
|
||||
mobileOrder?: number;
|
||||
props: { [key: string]: any };
|
||||
}[];
|
||||
};
|
||||
@@ -40,6 +43,7 @@ export interface Customization {
|
||||
|
||||
export interface Env {
|
||||
TESTNET_ENABLED: boolean;
|
||||
TESTNET4_ENABLED: boolean;
|
||||
SIGNET_ENABLED: boolean;
|
||||
LIQUID_ENABLED: boolean;
|
||||
LIQUID_TESTNET_ENABLED: boolean;
|
||||
@@ -73,6 +77,7 @@ export interface Env {
|
||||
|
||||
const defaultEnv: Env = {
|
||||
'TESTNET_ENABLED': false,
|
||||
'TESTNET4_ENABLED': false,
|
||||
'SIGNET_ENABLED': false,
|
||||
'LIQUID_ENABLED': false,
|
||||
'LIQUID_TESTNET_ENABLED': false,
|
||||
@@ -127,9 +132,10 @@ export class StateService {
|
||||
bsqPrice$ = new ReplaySubject<number>(1);
|
||||
mempoolInfo$ = new ReplaySubject<MempoolInfo>(1);
|
||||
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
|
||||
mempoolBlockTransactions$ = new Subject<TransactionStripped[]>();
|
||||
mempoolBlockDelta$ = new Subject<MempoolBlockDelta>();
|
||||
mempoolBlockUpdate$ = new Subject<MempoolBlockUpdate>();
|
||||
liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>;
|
||||
accelerations$ = new Subject<AccelerationDelta>();
|
||||
liveAccelerations$: Observable<Acceleration[]>;
|
||||
txConfirmed$ = new Subject<[string, BlockExtended]>();
|
||||
txReplaced$ = new Subject<ReplacedTransaction>();
|
||||
txRbfInfo$ = new Subject<RbfTree>();
|
||||
@@ -215,30 +221,48 @@ export class StateService {
|
||||
this.router.navigate(['/tracker/' + window.location.pathname.slice(4)]);
|
||||
}
|
||||
|
||||
this.liveMempoolBlockTransactions$ = merge(
|
||||
this.mempoolBlockTransactions$.pipe(map(transactions => { return { transactions }; })),
|
||||
this.mempoolBlockDelta$.pipe(map(delta => { return { delta }; })),
|
||||
).pipe(scan((transactions: { [txid: string]: TransactionStripped }, change: any): { [txid: string]: TransactionStripped } => {
|
||||
if (change.transactions) {
|
||||
const txMap = {}
|
||||
this.liveMempoolBlockTransactions$ = this.mempoolBlockUpdate$.pipe(scan((transactions: { [txid: string]: TransactionStripped }, change: MempoolBlockUpdate): { [txid: string]: TransactionStripped } => {
|
||||
if (isMempoolState(change)) {
|
||||
const txMap = {};
|
||||
change.transactions.forEach(tx => {
|
||||
txMap[tx.txid] = tx;
|
||||
})
|
||||
});
|
||||
return txMap;
|
||||
} else {
|
||||
change.delta.changed.forEach(tx => {
|
||||
transactions[tx.txid].rate = tx.rate;
|
||||
})
|
||||
change.delta.removed.forEach(txid => {
|
||||
change.added.forEach(tx => {
|
||||
transactions[tx.txid] = tx;
|
||||
});
|
||||
change.removed.forEach(txid => {
|
||||
delete transactions[txid];
|
||||
});
|
||||
change.delta.added.forEach(tx => {
|
||||
transactions[tx.txid] = tx;
|
||||
change.changed.forEach(tx => {
|
||||
if (transactions[tx.txid]) {
|
||||
transactions[tx.txid].rate = tx.rate;
|
||||
transactions[tx.txid].acc = tx.acc;
|
||||
}
|
||||
});
|
||||
return transactions;
|
||||
}
|
||||
}, {}));
|
||||
|
||||
// Emits the full list of pending accelerations each time it changes
|
||||
this.liveAccelerations$ = this.accelerations$.pipe(
|
||||
scan((accelerations: { [txid: string]: Acceleration }, delta: AccelerationDelta) => {
|
||||
if (delta.reset) {
|
||||
accelerations = {};
|
||||
} else {
|
||||
for (const txid of delta.removed) {
|
||||
delete accelerations[txid];
|
||||
}
|
||||
}
|
||||
for (const acc of delta.added) {
|
||||
accelerations[acc.txid] = acc;
|
||||
}
|
||||
return accelerations;
|
||||
}, {}),
|
||||
map((accMap) => Object.values(accMap).sort((a,b) => b.added - a.added))
|
||||
);
|
||||
|
||||
this.networkChanged$.subscribe((network) => {
|
||||
this.transactions$ = new BehaviorSubject<TransactionStripped[]>(null);
|
||||
this.blocksSubject$.next([]);
|
||||
@@ -279,7 +303,7 @@ export class StateService {
|
||||
this.rateUnits$ = new BehaviorSubject<string>(rateUnitPreference || 'vb');
|
||||
|
||||
const blockDisplayModePreference = this.storageService.getValue('block-display-mode-preference');
|
||||
this.blockDisplayMode$ = new BehaviorSubject<string>(blockDisplayModePreference || 'size');
|
||||
this.blockDisplayMode$ = new BehaviorSubject<string>(blockDisplayModePreference || 'fees');
|
||||
|
||||
const viewAmountModePreference = this.storageService.getValue('view-amount-mode') as 'btc' | 'sats' | 'fiat';
|
||||
this.viewAmountMode$ = new BehaviorSubject<'btc' | 'sats' | 'fiat'>(viewAmountModePreference || 'btc');
|
||||
@@ -299,7 +323,7 @@ export class StateService {
|
||||
// (?:preview\/)? optional "preview" prefix (non-capturing)
|
||||
// (testnet|signet)/ network string (captured as networkMatches[1])
|
||||
// ($|\/) network string must end or end with a slash
|
||||
const networkMatches = url.match(/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?(?:preview\/)?(testnet|signet)($|\/)/);
|
||||
const networkMatches = url.match(/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?(?:preview\/)?(testnet4?|signet)($|\/)/);
|
||||
switch (networkMatches && networkMatches[1]) {
|
||||
case 'signet':
|
||||
if (this.network !== 'signet') {
|
||||
@@ -318,6 +342,12 @@ export class StateService {
|
||||
}
|
||||
}
|
||||
return;
|
||||
case 'testnet4':
|
||||
if (this.network !== 'testnet4') {
|
||||
this.network = 'testnet4';
|
||||
this.networkChanged$.next('testnet4');
|
||||
}
|
||||
return;
|
||||
default:
|
||||
if (this.env.BASE_MODULE !== 'mempool') {
|
||||
if (this.network !== this.env.BASE_MODULE) {
|
||||
@@ -366,7 +396,7 @@ export class StateService {
|
||||
}
|
||||
|
||||
isAnyTestnet(): boolean {
|
||||
return ['testnet', 'signet', 'liquidtestnet'].includes(this.network);
|
||||
return ['testnet', 'testnet4', 'signet', 'liquidtestnet'].includes(this.network);
|
||||
}
|
||||
|
||||
resetChainTip() {
|
||||
|
||||
@@ -17,14 +17,14 @@ export class ThemeService {
|
||||
private storageService: StorageService,
|
||||
private stateService: StateService,
|
||||
) {
|
||||
const theme = this.storageService.getValue('theme-preference') || this.stateService.env.customize?.theme || 'default';
|
||||
const theme = this.stateService.env.customize?.theme || this.storageService.getValue('theme-preference') || 'default';
|
||||
this.apply(theme);
|
||||
}
|
||||
|
||||
apply(theme) {
|
||||
this.theme = theme;
|
||||
if (theme !== 'default') {
|
||||
theme === 'contrast' ? this.mempoolFeeColors = contrastMempoolFeeColors : this.mempoolFeeColors = defaultMempoolFeeColors;
|
||||
theme === 'contrast' || theme === 'bukele' ? this.mempoolFeeColors = contrastMempoolFeeColors : this.mempoolFeeColors = defaultMempoolFeeColors;
|
||||
try {
|
||||
if (!this.style) {
|
||||
this.style = document.createElement('link');
|
||||
@@ -44,7 +44,9 @@ export class ThemeService {
|
||||
this.style = null;
|
||||
}
|
||||
}
|
||||
this.storageService.setValue('theme-preference', theme);
|
||||
if (!this.stateService.env.customize?.theme) {
|
||||
this.storageService.setValue('theme-preference', theme);
|
||||
}
|
||||
this.themeChanged$.next(this.theme);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export class WebsocketService {
|
||||
private isTrackingRbfSummary = false;
|
||||
private isTrackingAddress: string | false = false;
|
||||
private isTrackingAddresses: string[] | false = false;
|
||||
private isTrackingAccelerations: boolean = false;
|
||||
private trackingMempoolBlock: number;
|
||||
private latestGitCommit = '';
|
||||
private onlineCheckTimeout: number;
|
||||
@@ -132,6 +133,9 @@ export class WebsocketService {
|
||||
if (this.isTrackingAddresses) {
|
||||
this.startTrackAddresses(this.isTrackingAddresses);
|
||||
}
|
||||
if (this.isTrackingAccelerations) {
|
||||
this.startTrackAccelerations();
|
||||
}
|
||||
this.stateService.connectionState$.next(2);
|
||||
}
|
||||
|
||||
@@ -235,6 +239,24 @@ export class WebsocketService {
|
||||
this.isTrackingRbfSummary = false;
|
||||
}
|
||||
|
||||
startTrackAccelerations() {
|
||||
this.websocketSubject.next({ 'track-accelerations': true });
|
||||
this.isTrackingAccelerations = true;
|
||||
}
|
||||
|
||||
stopTrackAccelerations() {
|
||||
if (this.isTrackingAccelerations) {
|
||||
this.websocketSubject.next({ 'track-accelerations': false });
|
||||
this.isTrackingAccelerations = false;
|
||||
}
|
||||
}
|
||||
|
||||
ensureTrackAccelerations() {
|
||||
if (!this.isTrackingAccelerations) {
|
||||
this.startTrackAccelerations();
|
||||
}
|
||||
}
|
||||
|
||||
fetchStatistics(historicalDate: string) {
|
||||
this.websocketSubject.next({ historicalDate });
|
||||
}
|
||||
@@ -401,19 +423,33 @@ export class WebsocketService {
|
||||
if (response['projected-block-transactions'].index == this.trackingMempoolBlock) {
|
||||
if (response['projected-block-transactions'].blockTransactions) {
|
||||
this.stateService.mempoolSequence = response['projected-block-transactions'].sequence;
|
||||
this.stateService.mempoolBlockTransactions$.next(response['projected-block-transactions'].blockTransactions.map(uncompressTx));
|
||||
this.stateService.mempoolBlockUpdate$.next({
|
||||
transactions: response['projected-block-transactions'].blockTransactions.map(uncompressTx),
|
||||
});
|
||||
} else if (response['projected-block-transactions'].delta) {
|
||||
if (this.stateService.mempoolSequence && response['projected-block-transactions'].sequence !== this.stateService.mempoolSequence + 1) {
|
||||
this.stateService.mempoolSequence = 0;
|
||||
this.startTrackMempoolBlock(this.trackingMempoolBlock, true);
|
||||
} else {
|
||||
this.stateService.mempoolSequence = response['projected-block-transactions'].sequence;
|
||||
this.stateService.mempoolBlockDelta$.next(uncompressDeltaChange(response['projected-block-transactions'].delta));
|
||||
this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(response['projected-block-transactions'].delta));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (response['accelerations']) {
|
||||
if (response['accelerations'].accelerations) {
|
||||
this.stateService.accelerations$.next({
|
||||
added: response['accelerations'].accelerations,
|
||||
removed: [],
|
||||
reset: true,
|
||||
});
|
||||
} else {
|
||||
this.stateService.accelerations$.next(response['accelerations']);
|
||||
}
|
||||
}
|
||||
|
||||
if (response['live-2h-chart']) {
|
||||
this.stateService.live2Chart$.next(response['live-2h-chart']);
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ export function nextRoundNumber(num: number): number {
|
||||
export function seoDescriptionNetwork(network: string): string {
|
||||
if( network === 'liquidtestnet' || network === 'testnet' ) {
|
||||
return ' Testnet';
|
||||
} else if( network === 'signet' || network === 'testnet' ) {
|
||||
} else if( network === 'signet' || network === 'testnet' || network === 'testnet4') {
|
||||
return ' ' + network.charAt(0).toUpperCase() + network.slice(1);
|
||||
}
|
||||
return '';
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
<ng-template [ngIf]="network === 'liquid'">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>{{ unit }}
|
||||
</span>
|
||||
@@ -4,13 +4,19 @@
|
||||
<div class="row main" [class]="{'services': isServicesPage}">
|
||||
<div class="col-md-12 branding mt-2">
|
||||
<div class="main-logo" [class]="{'services': isServicesPage}">
|
||||
<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>
|
||||
@if (enterpriseInfo?.footer_img) {
|
||||
<img [src]="enterpriseInfo?.footer_img" alt="enterpriseInfo.title" height="60px" class="mr-3">
|
||||
} @else {
|
||||
<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>
|
||||
}
|
||||
</div>
|
||||
<p class="explore-tagline-mobile">
|
||||
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
|
||||
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">®</ng-template>
|
||||
</p>
|
||||
@if (!enterpriseInfo?.footer_img) {
|
||||
<p class="explore-tagline-mobile">
|
||||
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
|
||||
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">®</ng-template>
|
||||
</p>
|
||||
}
|
||||
<div class="site-options language-selector d-flex justify-content-center align-items-center" [class]="{'services': isServicesPage}">
|
||||
<div class="selector">
|
||||
<app-language-selector></app-language-selector>
|
||||
@@ -21,34 +27,41 @@
|
||||
<div class="selector">
|
||||
<app-rate-unit-selector></app-rate-unit-selector>
|
||||
</div>
|
||||
<div class="selector d-none d-sm-flex">
|
||||
<app-theme-selector></app-theme-selector>
|
||||
</div>
|
||||
@if (!env.customize?.theme) {
|
||||
<div class="selector d-none d-sm-flex">
|
||||
<app-theme-selector></app-theme-selector>
|
||||
</div>
|
||||
}
|
||||
<a *ngIf="stateService.isMempoolSpaceBuild" class="btn btn-purple sponsor d-none d-sm-flex justify-content-center" [routerLink]="['/login' | relativeUrl]">
|
||||
<span *ngIf="loggedIn" i18n="shared.my-account" class="nowrap">My Account</span>
|
||||
<span *ngIf="!loggedIn" i18n="shared.sign-in" class="nowrap">Sign In</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="selector d-flex d-sm-none justify-content-center ml-auto mr-auto mt-0">
|
||||
<app-theme-selector></app-theme-selector>
|
||||
</div>
|
||||
<a *ngIf="stateService.isMempoolSpaceBuild" class="btn btn-purple sponsor d-flex d-sm-none justify-content-center ml-auto mr-auto mt-0 mb-2" [routerLink]="['/login' | relativeUrl]">
|
||||
<span *ngIf="loggedIn" i18n="shared.my-account" class="nowrap">My Account</span>
|
||||
<span *ngIf="!loggedIn" i18n="shared.sign-in" class="nowrap">Sign In</span>
|
||||
</a>
|
||||
<p class="explore-tagline-desktop">
|
||||
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
|
||||
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">®</ng-template>
|
||||
</p>
|
||||
@if (!env.customize?.theme) {
|
||||
<div class="selector d-flex d-sm-none justify-content-center ml-auto mr-auto mt-0">
|
||||
<app-theme-selector></app-theme-selector>
|
||||
</div>
|
||||
}
|
||||
@if (!enterpriseInfo?.footer_img) {
|
||||
<a *ngIf="stateService.isMempoolSpaceBuild" class="btn btn-purple sponsor d-flex d-sm-none justify-content-center ml-auto mr-auto mt-0 mb-2" [routerLink]="['/login' | relativeUrl]">
|
||||
<span *ngIf="loggedIn" i18n="shared.my-account" class="nowrap">My Account</span>
|
||||
<span *ngIf="!loggedIn" i18n="shared.sign-in" class="nowrap">Sign In</span>
|
||||
</a>
|
||||
<p class="explore-tagline-desktop">
|
||||
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
|
||||
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">®</ng-template>
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row col-md-12 link-tree" [class]="{'services': isServicesPage}">
|
||||
<div class="links">
|
||||
<p class="category" i18n="footer.explore">Explore</p>
|
||||
<p><a *ngIf="env.MINING_DASHBOARD" [routerLink]="['/mining' | relativeUrl]" i18n="mining.mining-dashboard">Mining Dashboard</a></p>
|
||||
<p><a *ngIf="env.LIGHTNING" [routerLink]="['/lightning' | relativeUrl]" i18n="master-page.lightning">Lightning Explorer</a></p>
|
||||
<p><a *ngIf="env.LIGHTNING && lightningNetworks.includes(stateService.network)" [routerLink]="['/lightning' | relativeUrl]" i18n="master-page.lightning">Lightning Explorer</a></p>
|
||||
<p><a [routerLink]="['/blocks' | relativeUrl]" i18n="dashboard.recent-blocks">Recent Blocks</a></p>
|
||||
<p><a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a></p>
|
||||
<p><a [routerLink]="['/tx/test' | relativeUrl]" i18n="shared.test-transaction|Test Transaction">Test Transaction</a></p>
|
||||
<p *ngIf="officialMempoolSpace"><a [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]" i18n="footer.connect-to-our-nodes">Connect to our Nodes</a></p>
|
||||
<p><a [routerLink]="['/docs/api' | relativeUrl]" i18n="footer.api-documentation">API Documentation</a></p>
|
||||
</div>
|
||||
@@ -61,10 +74,11 @@
|
||||
<p><a [routerLink]="['/docs/faq' | relativeUrl]" i18n="faq.more-faq">More FAQs »</a></p>
|
||||
</div>
|
||||
|
||||
<div class="links" *ngIf="officialMempoolSpace || env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.LIQUID_TESTNET_ENABLED else toolBox" >
|
||||
<div class="links" *ngIf="officialMempoolSpace || env.TESTNET_ENABLED || env.TESTNET4_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.LIQUID_TESTNET_ENABLED else toolBox" >
|
||||
<p class="category" i18n="footer.networks">Networks</p>
|
||||
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== '') && (currentNetwork !== 'mainnet')"><a [href]="networkLink('mainnet')" i18n="footer.mainnet-explorer">Mainnet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'testnet') && env.TESTNET_ENABLED"><a [href]="networkLink('testnet')" i18n="footer.testnet-explorer">Testnet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'testnet') && env.TESTNET_ENABLED"><a [href]="networkLink('testnet')" i18n="footer.testnet3-explorer">Testnet3 Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'testnet4') && env.TESTNET4_ENABLED"><a [href]="networkLink('testnet4')" i18n="footer.testnet4-explorer">Testnet4 Explorer</a> <span class="badge badge-pill badge-warning beta-network" i18n="beta">beta</span></p>
|
||||
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'signet') && env.SIGNET_ENABLED"><a [href]="networkLink('signet')" i18n="footer.signet-explorer">Signet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || env.LIQUID_ENABLED) && (currentNetwork !== 'liquidtestnet')"><a [href]="networkLink('liquidtestnet')" i18n="footer.liquid-testnet-explorer">Liquid Testnet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || env.LIQUID_ENABLED) && (currentNetwork !== 'liquid')"><a [href]="networkLink('liquid')" i18n="footer.liquid-explorer">Liquid Explorer</a></p>
|
||||
|
||||
@@ -329,3 +329,8 @@ footer .nowrap {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.beta-network {
|
||||
font-size: 8px;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, Inject, LOCALE_ID, HostListener } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, Inject, LOCALE_ID, HostListener, OnDestroy } from '@angular/core';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Observable, merge, of, Subject, Subscription } from 'rxjs';
|
||||
import { tap, takeUntil } from 'rxjs/operators';
|
||||
@@ -8,6 +8,7 @@ import { LanguageService } from '../../../services/language.service';
|
||||
import { NavigationService } from '../../../services/navigation.service';
|
||||
import { StorageService } from '../../../services/storage.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
import { EnterpriseService } from '../../../services/enterprise.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-global-footer',
|
||||
@@ -15,7 +16,7 @@ import { WebsocketService } from '../../../services/websocket.service';
|
||||
styleUrls: ['./global-footer.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class GlobalFooterComponent implements OnInit {
|
||||
export class GlobalFooterComponent implements OnInit, OnDestroy {
|
||||
private destroy$: Subject<any> = new Subject<any>();
|
||||
env: Env;
|
||||
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||
@@ -27,14 +28,19 @@ export class GlobalFooterComponent implements OnInit {
|
||||
network$: Observable<string>;
|
||||
networkPaths: { [network: string]: string };
|
||||
currentNetwork = '';
|
||||
lightningNetworks = ['', 'mainnet', 'bitcoin', 'testnet', 'signet'];
|
||||
loggedIn = false;
|
||||
urlSubscription: Subscription;
|
||||
isServicesPage = false;
|
||||
|
||||
enterpriseInfo: any;
|
||||
enterpriseInfo$: Subscription;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private languageService: LanguageService,
|
||||
private navigationService: NavigationService,
|
||||
private enterpriseService: EnterpriseService,
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private storageService: StorageService,
|
||||
private route: ActivatedRoute,
|
||||
@@ -53,6 +59,9 @@ export class GlobalFooterComponent implements OnInit {
|
||||
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||
this.networkPaths = paths;
|
||||
});
|
||||
this.enterpriseInfo$ = this.enterpriseService.info$.subscribe(info => {
|
||||
this.enterpriseInfo = info;
|
||||
});
|
||||
this.network$ = merge(of(''), this.stateService.networkChanged$).pipe(
|
||||
tap((network: string) => {
|
||||
return network;
|
||||
@@ -72,11 +81,14 @@ export class GlobalFooterComponent implements OnInit {
|
||||
this.destroy$.next(true);
|
||||
this.destroy$.complete();
|
||||
this.urlSubscription.unsubscribe();
|
||||
if (this.enterpriseInfo$) {
|
||||
this.enterpriseInfo$.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
networkLink(network) {
|
||||
const thisNetwork = network || 'mainnet';
|
||||
if( network === '' || network === 'mainnet' || network === 'testnet' || network === 'signet' ) {
|
||||
if( network === '' || network === 'mainnet' || network === 'testnet' || network === 'testnet4' || network === 'signet' ) {
|
||||
return (this.env.BASE_MODULE === 'mempool' ? '' : this.env.MEMPOOL_WEBSITE_URL + this.urlLanguage) + this.networkPaths[thisNetwork] || '/';
|
||||
}
|
||||
if( network === 'liquid' || network === 'liquidtestnet' ) {
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
<ng-template [ngIf]="network === 'liquid'">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>sats
|
||||
</span>
|
||||
@@ -1,6 +1,14 @@
|
||||
<div class="container p-lg-0 pb-0" style="max-width: 100%; margin-top: 7px" *ngIf="storageService.getValue('hideWarning') !== 'hidden'">
|
||||
<div class="alert alert-danger mb-0 text-center">
|
||||
<div class="message-container" i18n="warning-testnet">This is a test network. Coins have no value.</div>
|
||||
<div class="message-container">
|
||||
<span i18n="warning-testnet">This is a test network. Coins have no value.</span>
|
||||
@if (stateService.network === 'testnet') {
|
||||
<span i18n="testnet3-deprecated">Testnet3 is deprecated, and will soon be replaced by <a href="/testnet4">Testnet4</a></span>
|
||||
}
|
||||
@if (stateService.network === 'testnet4') {
|
||||
<span i18n="testnet4-not-finalized">Testnet4 is not yet finalized, and may be reset at anytime.</span>
|
||||
}
|
||||
</div>
|
||||
<button type="button" class="close" (click)="dismissWarning()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { StorageService } from '../../../services/storage.service';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-testnet-alert',
|
||||
@@ -11,6 +12,7 @@ export class TestnetAlertComponent {
|
||||
|
||||
constructor(
|
||||
public storageService: StorageService,
|
||||
public stateService: StateService,
|
||||
) { }
|
||||
|
||||
dismissWarning(): void {
|
||||
|
||||
@@ -62,6 +62,20 @@ const ADDRESS_CHARS: {
|
||||
+ `{20,100}`
|
||||
+ `)`,
|
||||
},
|
||||
testnet4: {
|
||||
base58: `[mn2]` // Starts with a single m, n, or 2 (P2PKH is m or n, 2 is P2SH)
|
||||
+ BASE58_CHARS
|
||||
+ `{33,34}`, // m|n is 34 length, 2 is 35 length (We match the first letter separately)
|
||||
bech32: `(?:`
|
||||
+ `tb1` // Starts with tb1
|
||||
+ BECH32_CHARS_LW
|
||||
+ `{20,100}` // As per bech32, 6 char checksum is minimum
|
||||
+ `|`
|
||||
+ `TB1` // All upper case version
|
||||
+ BECH32_CHARS_UP
|
||||
+ `{20,100}`
|
||||
+ `)`,
|
||||
},
|
||||
signet: {
|
||||
base58: `[mn2]`
|
||||
+ BASE58_CHARS
|
||||
@@ -128,7 +142,7 @@ const ADDRESS_CHARS: {
|
||||
type RegexTypeNoAddrNoBlockHash = | `transaction` | `blockheight` | `date` | `timestamp`;
|
||||
export type RegexType = `address` | `blockhash` | RegexTypeNoAddrNoBlockHash;
|
||||
|
||||
export const NETWORKS = [`testnet`, `signet`, `liquid`, `liquidtestnet`, `mainnet`] as const;
|
||||
export const NETWORKS = [`testnet`, `testnet4`, `signet`, `liquid`, `liquidtestnet`, `mainnet`] as const;
|
||||
export type Network = typeof NETWORKS[number]; // Turn const array into union type
|
||||
|
||||
export const ADDRESS_REGEXES: [RegExp, Network][] = NETWORKS
|
||||
@@ -144,6 +158,8 @@ function isNetworkAvailable(network: Network, env: Env): boolean {
|
||||
switch (network) {
|
||||
case 'testnet':
|
||||
return env.TESTNET_ENABLED === true;
|
||||
case 'testnet4':
|
||||
return env.TESTNET4_ENABLED === true;
|
||||
case 'signet':
|
||||
return env.SIGNET_ENABLED === true;
|
||||
case 'liquid':
|
||||
@@ -160,7 +176,7 @@ function isNetworkAvailable(network: Network, env: Env): boolean {
|
||||
export function needBaseModuleChange(fromBaseModule: 'mempool' | 'liquid', toNetwork: Network): boolean {
|
||||
if (!toNetwork) return false; // No target network means no change needed
|
||||
if (fromBaseModule === 'mempool') {
|
||||
return toNetwork !== 'mainnet' && toNetwork !== 'testnet' && toNetwork !== 'signet';
|
||||
return toNetwork !== 'mainnet' && toNetwork !== 'testnet' && toNetwork !== 'testnet4' && toNetwork !== 'signet';
|
||||
}
|
||||
if (fromBaseModule === 'liquid') {
|
||||
return toNetwork !== 'liquid' && toNetwork !== 'liquidtestnet';
|
||||
@@ -175,7 +191,7 @@ export function getTargetUrl(toNetwork: Network, address: string, env: Env): str
|
||||
targetUrl += '/address/';
|
||||
targetUrl += address;
|
||||
}
|
||||
if (toNetwork === 'mainnet' || toNetwork === 'testnet' || toNetwork === 'signet') {
|
||||
if (toNetwork === 'mainnet' || toNetwork === 'testnet' || toNetwork === 'testnet4' || toNetwork === 'signet') {
|
||||
targetUrl = env.MEMPOOL_WEBSITE_URL;
|
||||
targetUrl += (toNetwork === 'mainnet' ? '' : `/${toNetwork}`);
|
||||
targetUrl += '/address/';
|
||||
@@ -209,6 +225,9 @@ export function getRegex(type: RegexType, network?: Network): RegExp {
|
||||
case `testnet`:
|
||||
leadingZeroes = 8; // Assumes at least 32 bits of difficulty
|
||||
break;
|
||||
case `testnet4`:
|
||||
leadingZeroes = 8; // Assumes at least 32 bits of difficulty
|
||||
break;
|
||||
case `signet`:
|
||||
leadingZeroes = 5;
|
||||
break;
|
||||
@@ -261,6 +280,15 @@ export function getRegex(type: RegexType, network?: Network): RegExp {
|
||||
regex += `|`; // OR
|
||||
regex += `(?:02|03)${HEX_CHARS}{64}`; // Compressed pubkey
|
||||
break;
|
||||
case `testnet4`:
|
||||
regex += ADDRESS_CHARS.testnet.base58;
|
||||
regex += `|`; // OR
|
||||
regex += ADDRESS_CHARS.testnet.bech32;
|
||||
regex += `|`; // OR
|
||||
regex += `04${HEX_CHARS}{128}`; // Uncompressed pubkey
|
||||
regex += `|`; // OR
|
||||
regex += `(?:02|03)${HEX_CHARS}{64}`; // Compressed pubkey
|
||||
break;
|
||||
case `signet`:
|
||||
regex += ADDRESS_CHARS.signet.base58;
|
||||
regex += `|`; // OR
|
||||
|
||||
@@ -70,6 +70,7 @@ import { AddressTransactionsWidgetComponent } from '../components/address-transa
|
||||
import { RbfTimelineComponent } from '../components/rbf-timeline/rbf-timeline.component';
|
||||
import { RbfTimelineTooltipComponent } from '../components/rbf-timeline/rbf-timeline-tooltip.component';
|
||||
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
|
||||
import { TestTransactionsComponent } from '../components/test-transactions/test-transactions.component';
|
||||
import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component';
|
||||
import { AssetGroupComponent } from '../components/assets/asset-group/asset-group.component';
|
||||
import { AssetCirculationComponent } from '../components/asset-circulation/asset-circulation.component';
|
||||
@@ -112,6 +113,7 @@ import { ClockComponent } from '../components/clock/clock.component';
|
||||
import { CalculatorComponent } from '../components/calculator/calculator.component';
|
||||
import { BitcoinsatoshisPipe } from '../shared/pipes/bitcoinsatoshis.pipe';
|
||||
import { HttpErrorComponent } from '../shared/components/http-error/http-error.component';
|
||||
import { TwitterWidgetComponent } from '../components/twitter-widget/twitter-widget.component';
|
||||
|
||||
import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-directives/weight-directives';
|
||||
|
||||
@@ -180,6 +182,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
||||
RbfTimelineComponent,
|
||||
RbfTimelineTooltipComponent,
|
||||
PushTransactionComponent,
|
||||
TestTransactionsComponent,
|
||||
AssetsNavComponent,
|
||||
AssetsFeaturedComponent,
|
||||
AssetGroupComponent,
|
||||
@@ -224,6 +227,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
||||
AccelerateCheckout,
|
||||
PendingStatsComponent,
|
||||
HttpErrorComponent,
|
||||
TwitterWidgetComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -318,6 +322,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
||||
RbfTimelineComponent,
|
||||
RbfTimelineTooltipComponent,
|
||||
PushTransactionComponent,
|
||||
TestTransactionsComponent,
|
||||
AssetsNavComponent,
|
||||
AssetsFeaturedComponent,
|
||||
AssetGroupComponent,
|
||||
@@ -351,6 +356,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
||||
AccelerateCheckout,
|
||||
PendingStatsComponent,
|
||||
HttpErrorComponent,
|
||||
TwitterWidgetComponent,
|
||||
|
||||
MempoolBlockOverviewComponent,
|
||||
ClockchainComponent,
|
||||
|
||||
@@ -3,30 +3,30 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Oficina Nacional del Bitcoin - Bitcoin Explorer</title>
|
||||
<title>National Bitcoin Office of El Salvador</title>
|
||||
<script src="/resources/config.js"></script>
|
||||
<script src="/resources/customize.js"></script>
|
||||
<base href="/">
|
||||
|
||||
<meta name="description" content="Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See the real-time status of your transactions, get network info, and more." />
|
||||
<meta property="og:image" content="https://mempool.space/resources/sv/onbtc-preview.jpg" />
|
||||
<meta name="description" content="The National Bitcoin Office (ONBTC) of El Salvador under President @nayibbukele" />
|
||||
<meta property="og:image" content="https://mempool.space/resources/onbtc/onbtc-preview.jpg" />
|
||||
<meta property="og:image:type" content="image/jpeg" />
|
||||
<meta property="og:image:width" content="2000" />
|
||||
<meta property="og:image:height" content="1000" />
|
||||
<meta property="og:description" content="Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See the real-time status of your transactions, get network info, and more." />
|
||||
<meta property="og:description" content="The National Bitcoin Office (ONBTC) of El Salvador under President @nayibbukele" />
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@mempool">
|
||||
<meta name="twitter:creator" content="@mempool">
|
||||
<meta name="twitter:title" content="Oficina Nacional del Bitcoin - Bitcoin Explorer">
|
||||
<meta name="twitter:description" content="Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See the real-time status of your transactions, get network info, and more." />
|
||||
<meta name="twitter:image" content="https://mempool.space/resources/sv/onbtc-preview.jpg" />
|
||||
<meta name="twitter:title" content="National Bitcoin Office of El Salvador">
|
||||
<meta name="twitter:description" content="The National Bitcoin Office (ONBTC) of El Salvador under President @nayibbukele" />
|
||||
<meta name="twitter:image" content="https://mempool.space/resources/onbtc/onbtc-preview.jpg" />
|
||||
<meta name="twitter:domain" content="bitcoin.gob.sv">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/resources/sv/favicons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/resources/sv/favicons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/resources/sv/favicons/favicon-16x16.png">
|
||||
<link rel="manifest" href="/resources/sv/favicons/site.webmanifest">
|
||||
<link rel="shortcut icon" href="/resources/sv/favicons/favicon.ico">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/resources/onbtc/favicons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/resources/onbtc/favicons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/resources/onbtc/favicons/favicon-16x16.png">
|
||||
<link rel="manifest" href="/resources/onbtc/favicons/site.webmanifest">
|
||||
<link rel="shortcut icon" href="/resources/onbtc/favicons/favicon.ico">
|
||||
<link id="canonical" rel="canonical" href="https://bitcoin.gob.sv">
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
@@ -1,37 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="159.7426"
|
||||
height="162.47015"
|
||||
viewBox="0 0 42.26523 42.986894"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
sodipodi:docname="elsalvador.svg"
|
||||
inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
id="elsalvador"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview469"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.8463012"
|
||||
inkscape:cx="21.606989"
|
||||
inkscape:cy="67.982966"
|
||||
inkscape:window-width="1728"
|
||||
inkscape:window-height="1051"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="38"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg5" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<clipPath
|
||||
|
||||
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user