Merge branch 'master' into mononaut/x-widget

This commit is contained in:
softsimon
2024-05-08 22:56:03 +07:00
committed by GitHub
137 changed files with 2695 additions and 825 deletions

View File

@@ -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');
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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.';

View File

@@ -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,7 +27,9 @@ export interface Customization {
name: string;
site_id?: number;
title: string;
img: string;
img?: string;
header_img?: string;
footer_img?: string;
rounded_corner: boolean;
},
dashboard: {
@@ -41,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;
@@ -74,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,
@@ -128,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>();
@@ -216,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([]);
@@ -280,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');
@@ -300,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') {
@@ -319,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) {
@@ -367,7 +396,7 @@ export class StateService {
}
isAnyTestnet(): boolean {
return ['testnet', 'signet', 'liquidtestnet'].includes(this.network);
return ['testnet', 'testnet4', 'signet', 'liquidtestnet'].includes(this.network);
}
resetChainTip() {

View File

@@ -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);
}
}

View File

@@ -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']);
}